From 589ed11e4a35fa5643e8f3949beefb4c21ceb743 Mon Sep 17 00:00:00 2001 From: Dylan Deverill Date: Sun, 10 May 2026 19:28:03 -0700 Subject: [PATCH] Initial release --- .gitignore | 25 + CMakeLists.txt | 62 ++ LICENSE | 21 + README.md | 67 ++ resources/SteamControllerOFF.ico | Bin 0 -> 22350 bytes resources/SteamControllerON.ico | Bin 0 -> 22512 bytes resources/app.rc | 4 + resources/resource.h | 4 + src/app/ControllerManager.cpp | 113 +++ src/app/ControllerManager.h | 56 ++ src/app/TrackpadMouse.cpp | 93 ++ src/app/TrackpadMouse.h | 27 + src/app/TrayApp.cpp | 257 ++++++ src/app/TrayApp.h | 41 + src/app/VirtualController.cpp | 113 +++ src/app/VirtualController.h | 22 + src/app/main.cpp | 19 + src/hid/HidDevice.cpp | 210 +++++ src/hid/HidDevice.h | 47 ++ src/main.cpp | 165 ++++ src/probe/RawControllerProbe.cpp | 102 +++ src/steam/SteamController.cpp | 116 +++ src/steam/SteamController.h | 138 +++ .../.github/ISSUE_TEMPLATE/bug_report.md | 29 + .../.github/ISSUE_TEMPLATE/feature_request.md | 17 + .../ISSUE_TEMPLATE/you-need-support.md | 7 + .../pull_request_template.md | 1 + third_party/ViGEmClient-1.16.18.0/.gitignore | 45 + third_party/ViGEmClient-1.16.18.0/.nuke | 1 + third_party/ViGEmClient-1.16.18.0/LICENSE | 21 + third_party/ViGEmClient-1.16.18.0/README.md | 22 + .../ViGEmClient-1.16.18.0/ViGEmClient.sln | 53 ++ .../ViGEmClient-1.16.18.0/appveyor.yml | 9 + third_party/ViGEmClient-1.16.18.0/build.ps1 | 65 ++ third_party/ViGEmClient-1.16.18.0/build.sh | 68 ++ .../include/ViGEmClient.h | 495 +++++++++++ .../include/ViGEmCommon.h | 206 +++++ .../ViGEmClient-1.16.18.0/include/ViGEmUtil.h | 50 ++ .../include/km/ViGEmBusShared.h | 493 +++++++++++ .../ViGEmClient-1.16.18.0/src/README.md | 8 + .../ViGEmClient-1.16.18.0/src/ViGEmClient.cpp | 794 ++++++++++++++++++ .../ViGEmClient-1.16.18.0/src/ViGEmClient.rc | 100 +++ .../src/ViGEmClient.vcxproj | 313 +++++++ .../src/ViGEmClient.vcxproj.filters | 44 + .../ViGEmClient-1.16.18.0/src/resource.h | 14 + 45 files changed, 4557 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 resources/SteamControllerOFF.ico create mode 100644 resources/SteamControllerON.ico create mode 100644 resources/app.rc create mode 100644 resources/resource.h create mode 100644 src/app/ControllerManager.cpp create mode 100644 src/app/ControllerManager.h create mode 100644 src/app/TrackpadMouse.cpp create mode 100644 src/app/TrackpadMouse.h create mode 100644 src/app/TrayApp.cpp create mode 100644 src/app/TrayApp.h create mode 100644 src/app/VirtualController.cpp create mode 100644 src/app/VirtualController.h create mode 100644 src/app/main.cpp create mode 100644 src/hid/HidDevice.cpp create mode 100644 src/hid/HidDevice.h create mode 100644 src/main.cpp create mode 100644 src/probe/RawControllerProbe.cpp create mode 100644 src/steam/SteamController.cpp create mode 100644 src/steam/SteamController.h create mode 100644 third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/you-need-support.md create mode 100644 third_party/ViGEmClient-1.16.18.0/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 third_party/ViGEmClient-1.16.18.0/.gitignore create mode 100644 third_party/ViGEmClient-1.16.18.0/.nuke create mode 100644 third_party/ViGEmClient-1.16.18.0/LICENSE create mode 100644 third_party/ViGEmClient-1.16.18.0/README.md create mode 100644 third_party/ViGEmClient-1.16.18.0/ViGEmClient.sln create mode 100644 third_party/ViGEmClient-1.16.18.0/appveyor.yml create mode 100644 third_party/ViGEmClient-1.16.18.0/build.ps1 create mode 100644 third_party/ViGEmClient-1.16.18.0/build.sh create mode 100644 third_party/ViGEmClient-1.16.18.0/include/ViGEmClient.h create mode 100644 third_party/ViGEmClient-1.16.18.0/include/ViGEmCommon.h create mode 100644 third_party/ViGEmClient-1.16.18.0/include/ViGEmUtil.h create mode 100644 third_party/ViGEmClient-1.16.18.0/include/km/ViGEmBusShared.h create mode 100644 third_party/ViGEmClient-1.16.18.0/src/README.md create mode 100644 third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.cpp create mode 100644 third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.rc create mode 100644 third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj create mode 100644 third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj.filters create mode 100644 third_party/ViGEmClient-1.16.18.0/src/resource.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a53fc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Build output +build/ + +# Loose binaries committed by mistake +*.exe +*.dll +*.lib +*.pdb + +# Claude Code local settings +.claude/ + +# Reverse engineering session data +Results/ +HIDInfo +HIDReportDescriptor.rtf + +# CMake user files +CMakeSettings.json +CMakeUserPresets.json + +# Visual Studio +.vs/ +*.user +*.suo diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0aeb572 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.20) +project(SteamlessController CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Shared HID + Steam Controller library code +add_library(SteamController STATIC + src/hid/HidDevice.cpp + src/steam/SteamController.cpp +) +target_include_directories(SteamController PUBLIC src) +target_link_libraries(SteamController PUBLIC hid setupapi) +if (MSVC) + target_compile_options(SteamController PRIVATE /W4 /WX) +endif() + +# Diagnostic probe tool (console) +add_executable(SteamProbe + src/main.cpp +) +target_link_libraries(SteamProbe PRIVATE SteamController) +if (MSVC) + target_compile_options(SteamProbe PRIVATE /W4 /WX) + set_target_properties(SteamProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") +endif() + +# ViGEmClient (built from source, static) +add_library(ViGEmClient STATIC + third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.cpp +) +target_include_directories(ViGEmClient PUBLIC + third_party/ViGEmClient-1.16.18.0/include +) +target_link_libraries(ViGEmClient PUBLIC setupapi) +if (MSVC) + target_compile_options(ViGEmClient PRIVATE /W0) +endif() + +# RawGameController diagnostic probe (WinRT) +add_executable(RawControllerProbe + src/probe/RawControllerProbe.cpp +) +target_link_libraries(RawControllerProbe PRIVATE windowsapp) +if (MSVC) + target_compile_options(RawControllerProbe PRIVATE /W4) +endif() + +# System tray app (no console window) +add_executable(SteamlessController WIN32 + src/app/main.cpp + src/app/TrayApp.cpp + src/app/ControllerManager.cpp + src/app/VirtualController.cpp + src/app/TrackpadMouse.cpp + resources/app.rc +) +target_include_directories(SteamlessController PRIVATE resources) +target_link_libraries(SteamlessController PRIVATE SteamController ViGEmClient) +if (MSVC) + target_compile_options(SteamlessController PRIVATE /W4 /WX) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf6efcd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dylan Deverill + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0884265 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# SteamlessController + +A lightweight Windows system tray app that lets you use a **Steam Controller** as a standard gamepad — without Steam running. + +When **Steamless Mode** is active, the app disables the controller's built-in keyboard/mouse emulation (lizard mode) and exposes it as a virtual Xbox 360 controller via [ViGEmBus](https://github.com/nefarius/ViGEmBus), making it compatible with any game that supports XInput or the Xbox controller. + +## Features + +- System tray icon shows connection and mode status +- **Steamless Mode** — disables lizard mode and exposes controller as Xbox 360 gamepad +- **Trackpad Mouse** — use the right (or left) trackpad as a mouse cursor +- **Back Buttons for Clicking** — map R4/R5 (or L4/L5) to left/right mouse click +- **Use Left Trackpad Instead** — mirror all trackpad/back-button functionality to the left side for left-handed users +- **Start with Windows** — launch automatically at login +- Settings persist across restarts +- Single-instance guard — safe to leave running + +## Requirements + +### To run +- Windows 10 or later (64-bit) +- [ViGEmBus](https://github.com/nefarius/ViGEmBus/releases/latest) driver installed +- Steam Controller (VID `0x28DE` / PID `0x1302`) +- Steam **closed** (Steam claims the controller when running) + +### To build +- [Visual Studio 2022](https://visualstudio.microsoft.com/) with the **Desktop development with C++** workload +- [CMake](https://cmake.org/download/) 3.20 or later (included with Visual Studio, or install separately) +- Windows SDK 10.0.22000 or later (installed via Visual Studio Installer) + +## Building + +```bat +git clone https://github.com/your-username/SteamlessController.git +cd SteamlessController +cmake -B build +cmake --build build --target SteamlessController +``` + +The executable will be at `build\Debug\SteamlessController.exe`. + +For a release build: + +```bat +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --target SteamlessController +``` + +## CMake Targets + +| Target | Description | +|---|---| +| `SteamlessController` | Main system tray application | +| `SteamProbe` | Console diagnostic tool — dumps raw HID report bytes as you interact with the controller. Useful for protocol research. | +| `RawControllerProbe` | Checks whether `Windows.Gaming.Input.RawGameController` can enumerate the Steam Controller (requires WinRT). | + +## How it works + +The Steam Controller exposes a vendor HID collection (usage page `0xFF00`) that carries all game input in a 54-byte report (ID `0x42`) at ~60 Hz. By default the firmware runs in **lizard mode**, emulating a keyboard and mouse so the controller works without drivers. + +SteamlessController sends HID feature reports to disable lizard mode, then reads the raw input reports and translates them into a virtual Xbox 360 controller via ViGEmBus. A background heartbeat re-sends the disable command every 800 ms to keep lizard mode off. + +The full input report layout is documented in [`src/steam/SteamController.h`](src/steam/SteamController.h). + +## Third-party + +- [ViGEmClient](https://github.com/nefarius/ViGEmClient) — MIT License, built from source as a static library diff --git a/resources/SteamControllerOFF.ico b/resources/SteamControllerOFF.ico new file mode 100644 index 0000000000000000000000000000000000000000..b8df7283ed0c73b144a7f951cb2ab789c1c47914 GIT binary patch literal 22350 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 literal 0 HcmV?d00001 diff --git a/resources/SteamControllerON.ico b/resources/SteamControllerON.ico new file mode 100644 index 0000000000000000000000000000000000000000..4c5267638bd556acbd53398314544805069dbd34 GIT binary patch literal 22512 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 + +static std::unique_ptr g_ctrl; + +ControllerManager::ControllerManager(StateChangedFn onStateChanged) + : m_onStateChanged(std::move(onStateChanged)) +{ + TryOpen(); +} + +ControllerManager::~ControllerManager() { + Close(/*restoreLizard=*/true); +} + +void ControllerManager::OnDeviceChange() { + if (!m_connected) + TryOpen(); + else if (g_ctrl && !g_ctrl->IsOpen()) + Close(/*restoreLizard=*/false); +} + +void ControllerManager::EnableGameMode() { + if (!m_connected || m_gameModeActive) return; + if (!g_ctrl->DisableLizardMode()) return; + + m_virtual = std::make_unique(); + if (!m_virtual->IsValid()) { + bool missing = m_virtual->IsDriverMissing(); + m_virtual.reset(); + g_ctrl->EnableLizardMode(); + if (missing) m_onStateChanged(m_connected, m_gameModeActive, /*vigemMissing=*/true); + return; + } + + m_gameModeActive = true; + m_trackpad.Reset(); + m_trackpad.SetTrackpadEnabled(m_trackpadMouseEnabled); + m_trackpad.SetBackButtonsEnabled(m_backButtonsEnabled); + m_trackpad.SetUseLeftTrackpad(m_useLeftTrackpad); + StartReadLoop(); + m_onStateChanged(m_connected, m_gameModeActive, false); +} + +void ControllerManager::DisableGameMode() { + if (!m_gameModeActive) return; + StopReadLoop(); + m_trackpad.Reset(); + m_virtual.reset(); + g_ctrl->EnableLizardMode(); + m_gameModeActive = false; + m_onStateChanged(m_connected, m_gameModeActive, false); +} + +void ControllerManager::SetTrackpadMouseEnabled(bool enabled) { + m_trackpadMouseEnabled = enabled; + m_trackpad.SetTrackpadEnabled(enabled); +} + +void ControllerManager::SetBackButtonsEnabled(bool enabled) { + m_backButtonsEnabled = enabled; + m_trackpad.SetBackButtonsEnabled(enabled); +} + +void ControllerManager::SetUseLeftTrackpad(bool enabled) { + m_useLeftTrackpad = enabled; + m_trackpad.SetUseLeftTrackpad(enabled); +} + +void ControllerManager::TryOpen() { + if (!g_ctrl) g_ctrl = std::make_unique(); + if (g_ctrl->Open()) { + m_connected = true; + m_onStateChanged(m_connected, m_gameModeActive, false); + } +} + +void ControllerManager::Close(bool restoreLizard) { + StopReadLoop(); + m_virtual.reset(); + if (g_ctrl) { + if (restoreLizard && m_gameModeActive) + g_ctrl->EnableLizardMode(); + g_ctrl->Close(); + } + m_connected = false; + m_gameModeActive = false; + m_onStateChanged(m_connected, m_gameModeActive, false); +} + +void ControllerManager::StartReadLoop() { + m_readRunning = true; + m_readThread = std::thread(&ControllerManager::ReadLoop, this); +} + +void ControllerManager::StopReadLoop() { + m_readRunning = false; + if (m_readThread.joinable()) + m_readThread.join(); +} + +void ControllerManager::ReadLoop() { + uint8_t buf[64]; + while (m_readRunning) { + size_t n = g_ctrl->ReadReport(buf, sizeof(buf), /*timeoutMs=*/32); + if (n == 0) continue; + if (buf[0] != SteamController::REPORT_STATE) continue; + if (m_virtual) m_virtual->Update(buf, n); + m_trackpad.Update(buf, n); + } +} diff --git a/src/app/ControllerManager.h b/src/app/ControllerManager.h new file mode 100644 index 0000000..1d6a3eb --- /dev/null +++ b/src/app/ControllerManager.h @@ -0,0 +1,56 @@ +#pragma once +#include "TrackpadMouse.h" +#include +#include +#include +#include + +class VirtualController; + +// Manages the Steam Controller lifecycle: device discovery, lizard mode +// disable/enable, and the heartbeat that keeps lizard mode off. +// All public methods are safe to call from the UI thread. +class ControllerManager { +public: + using StateChangedFn = std::function; + + explicit ControllerManager(StateChangedFn onStateChanged); + ~ControllerManager(); + ControllerManager(const ControllerManager&) = delete; + ControllerManager& operator=(const ControllerManager&) = delete; + + // Called when Windows reports a device arrival or removal (WM_DEVICECHANGE). + void OnDeviceChange(); + + // Toggle game mode on/off. No-op if controller is not connected. + void EnableGameMode(); + void DisableGameMode(); + + void SetTrackpadMouseEnabled(bool enabled); + void SetBackButtonsEnabled(bool enabled); + void SetUseLeftTrackpad(bool enabled); + + bool IsConnected() const { return m_connected; } + bool IsGameModeActive() const { return m_gameModeActive; } + bool IsTrackpadMouseEnabled() const { return m_trackpadMouseEnabled; } + bool IsBackButtonsEnabled() const { return m_backButtonsEnabled; } + bool IsUseLeftTrackpad() const { return m_useLeftTrackpad; } + +private: + void TryOpen(); + void Close(bool restoreLizard); + void StartReadLoop(); + void StopReadLoop(); + void ReadLoop(); + + StateChangedFn m_onStateChanged; + bool m_connected = false; + bool m_gameModeActive = false; + bool m_trackpadMouseEnabled = false; + bool m_backButtonsEnabled = false; + bool m_useLeftTrackpad = false; + std::unique_ptr m_virtual; + TrackpadMouse m_trackpad; + std::thread m_readThread; + std::atomic m_readRunning{false}; +}; diff --git a/src/app/TrackpadMouse.cpp b/src/app/TrackpadMouse.cpp new file mode 100644 index 0000000..9fa9448 --- /dev/null +++ b/src/app/TrackpadMouse.cpp @@ -0,0 +1,93 @@ +#include "TrackpadMouse.h" +#include "steam/SteamController.h" +#include +#include + +static constexpr uint8_t BTN_TP_RT_CLICK = 0x40; // buf[4] bit 6 — hard press + +static void SendMouseButton(DWORD flags) { + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = flags; + SendInput(1, &input, sizeof(INPUT)); +} + +void TrackpadMouse::Reset() { + if (m_prevClick) SendMouseButton(MOUSEEVENTF_LEFTUP); + if (m_prevR4) SendMouseButton(MOUSEEVENTF_LEFTUP); + if (m_prevR5) SendMouseButton(MOUSEEVENTF_RIGHTUP); + m_touching = false; + m_prevClick = false; + m_prevR4 = false; + m_prevR5 = false; + m_prevX = 0; + m_prevY = 0; +} + +void TrackpadMouse::Update(const uint8_t* buf, size_t n) { + if (n < 30) return; + + const uint8_t b0 = buf[2]; + const uint8_t b1 = buf[3]; + const uint8_t b2 = buf[4]; + const uint8_t b3 = buf[5]; + + // --- Trackpad mouse movement and click --- + if (m_trackpadEnabled) { + const bool touching = m_useLeftTrackpad + ? (b3 & SteamController::BTN_TP_LT) != 0 + : (b2 & SteamController::BTN_TP_RT) != 0; + const bool clicking = m_useLeftTrackpad + ? (b3 & SteamController::BTN_TP_LT_CLICK) != 0 + : (b2 & BTN_TP_RT_CLICK) != 0; + + int16_t x = 0, y = 0; + if (m_useLeftTrackpad) { + memcpy(&x, buf + 18, 2); + memcpy(&y, buf + 20, 2); + } else { + memcpy(&x, buf + 24, 2); + memcpy(&y, buf + 26, 2); + } + + if (touching && m_touching) { + const int dx = static_cast(x - m_prevX); + const int dy = -static_cast(y - m_prevY); // negate: up = up + if (dx != 0 || dy != 0) { + INPUT input{}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = MOUSEEVENTF_MOVE; + input.mi.dx = static_cast(dx * SENSITIVITY); + input.mi.dy = static_cast(dy * SENSITIVITY); + SendInput(1, &input, sizeof(INPUT)); + } + } + + if (touching) { m_prevX = x; m_prevY = y; } + m_touching = touching; + + if (clicking != m_prevClick) { + SendMouseButton(clicking ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); + m_prevClick = clicking; + } + } + + // --- Back buttons: left side uses L4/L5, right side uses R4/R5 --- + if (m_backButtonsEnabled) { + const bool btn1 = m_useLeftTrackpad + ? (b2 & SteamController::BTN_L4) != 0 // L4 = left click + : (b0 & SteamController::BTN_R4) != 0; // R4 = left click + const bool btn2 = m_useLeftTrackpad + ? (b2 & SteamController::BTN_L5) != 0 // L5 = right click + : (b1 & SteamController::BTN_R5) != 0; // R5 = right click + + if (btn1 != m_prevR4) { + SendMouseButton(btn1 ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP); + m_prevR4 = btn1; + } + if (btn2 != m_prevR5) { + SendMouseButton(btn2 ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP); + m_prevR5 = btn2; + } + } +} diff --git a/src/app/TrackpadMouse.h b/src/app/TrackpadMouse.h new file mode 100644 index 0000000..e0eb903 --- /dev/null +++ b/src/app/TrackpadMouse.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +class TrackpadMouse { +public: + void SetTrackpadEnabled(bool enabled) { m_trackpadEnabled = enabled; } + void SetBackButtonsEnabled(bool enabled) { m_backButtonsEnabled = enabled; } + void SetUseLeftTrackpad(bool enabled) { m_useLeftTrackpad = enabled; } + + void Update(const uint8_t* buf, size_t n); + void Reset(); + +private: + bool m_trackpadEnabled = false; + bool m_backButtonsEnabled = false; + bool m_useLeftTrackpad = false; + + bool m_touching = false; + bool m_prevClick = false; + bool m_prevR4 = false; + bool m_prevR5 = false; + int16_t m_prevX = 0; + int16_t m_prevY = 0; + + static constexpr float SENSITIVITY = 0.015f; +}; diff --git a/src/app/TrayApp.cpp b/src/app/TrayApp.cpp new file mode 100644 index 0000000..9d96de2 --- /dev/null +++ b/src/app/TrayApp.cpp @@ -0,0 +1,257 @@ +#include "TrayApp.h" +#include "ControllerManager.h" +#include "resource.h" +#include +#include +#include + +static TrayApp* g_app = nullptr; + +static constexpr wchar_t WNDCLASS_NAME[] = L"SteamlessControllerTray"; + +TrayApp::TrayApp() { + g_app = this; +} + +TrayApp::~TrayApp() { + RemoveTrayIcon(); + g_app = nullptr; +} + +bool TrayApp::Init(HINSTANCE hInstance) { + m_hInstance = hInstance; + m_iconOff = LoadIconW(hInstance, MAKEINTRESOURCEW(IDI_ICON_OFF)); + m_iconOn = LoadIconW(hInstance, MAKEINTRESOURCEW(IDI_ICON_ON)); + m_wmTaskbar = RegisterWindowMessageW(L"TaskbarCreated"); + + WNDCLASSEXW wc{}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = WndProc; + wc.hInstance = hInstance; + wc.lpszClassName = WNDCLASS_NAME; + if (!RegisterClassExW(&wc)) return false; + + // Message-only window — invisible, never shown. + m_hwnd = CreateWindowExW(0, WNDCLASS_NAME, L"SteamlessController", + 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hInstance, nullptr); + if (!m_hwnd) return false; + + // Register for HID device arrival/removal notifications. + DEV_BROADCAST_DEVICEINTERFACE_W filter{}; + filter.dbcc_size = sizeof(filter); + filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; + // HID device interface GUID + filter.dbcc_classguid = {0x4D1E55B2, 0xF16F, 0x11CF, + {0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30}}; + RegisterDeviceNotificationW(m_hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE); + + m_controller = std::make_unique( + [this](bool connected, bool gameModeActive, bool vigemMissing) { + UpdateTrayIcon(connected, gameModeActive, vigemMissing); + }); + + LoadSettings(); + AddTrayIcon(); + return true; +} + +int TrayApp::Run() { + MSG msg; + BOOL ret; + while ((ret = GetMessageW(&msg, nullptr, 0, 0)) != 0) { + if (ret == -1) return -1; + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + return static_cast(msg.wParam); +} + +LRESULT CALLBACK TrayApp::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + if (g_app) return g_app->HandleMessage(hwnd, msg, wp, lp); + return DefWindowProcW(hwnd, msg, wp, lp); +} + +LRESULT TrayApp::HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { + if (msg == m_wmTaskbar) { + AddTrayIcon(); + return 0; + } + + switch (msg) { + case WM_TRAY: + if (LOWORD(lp) == NIN_BALLOONUSERCLICK) + ShellExecuteW(nullptr, L"open", L"https://github.com/nefarius/ViGEmBus/releases/latest", + nullptr, nullptr, SW_SHOWNORMAL); + else if (LOWORD(lp) == WM_RBUTTONUP || LOWORD(lp) == WM_LBUTTONUP) + ShowContextMenu(); + return 0; + + case WM_COMMAND: + switch (LOWORD(wp)) { + case IDM_TOGGLE: + if (m_controller->IsGameModeActive()) + m_controller->DisableGameMode(); + else + m_controller->EnableGameMode(); + break; + case IDM_TRACKPAD: + m_controller->SetTrackpadMouseEnabled(!m_controller->IsTrackpadMouseEnabled()); + SaveSettings(); + break; + case IDM_BACKBUTTONS: + m_controller->SetBackButtonsEnabled(!m_controller->IsBackButtonsEnabled()); + SaveSettings(); + break; + case IDM_LEFT_TRACKPAD: + m_controller->SetUseLeftTrackpad(!m_controller->IsUseLeftTrackpad()); + SaveSettings(); + break; + case IDM_EXIT: + m_controller->DisableGameMode(); + PostQuitMessage(0); + break; + } + return 0; + + case WM_DEVICECHANGE: + if (wp == DBT_DEVICEARRIVAL || wp == DBT_DEVICEREMOVECOMPLETE) + m_controller->OnDeviceChange(); + return TRUE; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(hwnd, msg, wp, lp); +} + +void TrayApp::AddTrayIcon() { + NOTIFYICONDATAW nid{}; + nid.cbSize = sizeof(nid); + nid.hWnd = m_hwnd; + nid.uID = TRAY_UID; + nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; + nid.uCallbackMessage = WM_TRAY; + nid.hIcon = m_iconOff; + wcscpy_s(nid.szTip, L"Steamless Controller"); + Shell_NotifyIconW(NIM_ADD, &nid); + nid.uVersion = NOTIFYICON_VERSION_4; + Shell_NotifyIconW(NIM_SETVERSION, &nid); +} + +void TrayApp::RemoveTrayIcon() { + NOTIFYICONDATAW nid{}; + nid.cbSize = sizeof(nid); + nid.hWnd = m_hwnd; + nid.uID = TRAY_UID; + Shell_NotifyIconW(NIM_DELETE, &nid); +} + +void TrayApp::UpdateTrayIcon(bool connected, bool gameModeActive, bool vigemMissing) { + if (vigemMissing) { ShowViGEmBalloon(); return; } + bool gameModeOn = gameModeActive; + + const wchar_t* tip = gameModeOn ? L"Steamless Controller — Steamless Mode ON" + : connected ? L"Steamless Controller — Connected (Steamless Mode OFF)" + : L"Steamless Controller — No controller found"; + + NOTIFYICONDATAW nid{}; + nid.cbSize = sizeof(nid); + nid.hWnd = m_hwnd; + nid.uID = TRAY_UID; + nid.uFlags = NIF_TIP | NIF_ICON; + nid.hIcon = gameModeOn ? m_iconOn : m_iconOff; + wcscpy_s(nid.szTip, tip); + Shell_NotifyIconW(NIM_MODIFY, &nid); +} + +void TrayApp::ShowViGEmBalloon() { + NOTIFYICONDATAW nid{}; + nid.cbSize = sizeof(nid); + nid.hWnd = m_hwnd; + nid.uID = TRAY_UID; + nid.uFlags = NIF_INFO; + nid.dwInfoFlags = NIIF_WARNING; + wcscpy_s(nid.szInfoTitle, L"Driver required"); + wcscpy_s(nid.szInfo, L"ViGEmBus is not installed. Click here to download it."); + Shell_NotifyIconW(NIM_MODIFY, &nid); +} + +static constexpr wchar_t REG_KEY[] = L"Software\\SteamlessController"; + +void TrayApp::LoadSettings() { + HKEY key; + if (RegOpenKeyExW(HKEY_CURRENT_USER, REG_KEY, 0, KEY_READ, &key) != ERROR_SUCCESS) + return; + + auto readBool = [&](const wchar_t* name, bool def) -> bool { + DWORD val = 0, size = sizeof(val); + if (RegQueryValueExW(key, name, nullptr, nullptr, + reinterpret_cast(&val), &size) == ERROR_SUCCESS) + return val != 0; + return def; + }; + + m_controller->SetTrackpadMouseEnabled(readBool(L"TrackpadMouse", false)); + m_controller->SetBackButtonsEnabled (readBool(L"BackButtons", false)); + m_controller->SetUseLeftTrackpad (readBool(L"UseLeftTrackpad", false)); + + RegCloseKey(key); +} + +void TrayApp::SaveSettings() { + HKEY key; + if (RegCreateKeyExW(HKEY_CURRENT_USER, REG_KEY, 0, nullptr, + REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, + &key, nullptr) != ERROR_SUCCESS) + return; + + auto writeBool = [&](const wchar_t* name, bool val) { + DWORD dw = val ? 1 : 0; + RegSetValueExW(key, name, 0, REG_DWORD, + reinterpret_cast(&dw), sizeof(dw)); + }; + + writeBool(L"TrackpadMouse", m_controller->IsTrackpadMouseEnabled()); + writeBool(L"BackButtons", m_controller->IsBackButtonsEnabled()); + writeBool(L"UseLeftTrackpad", m_controller->IsUseLeftTrackpad()); + + RegCloseKey(key); +} + +void TrayApp::ShowContextMenu() { + bool connected = m_controller->IsConnected(); + bool gameModeOn = m_controller->IsGameModeActive(); + bool trackpadOn = m_controller->IsTrackpadMouseEnabled(); + bool backButtonsOn = m_controller->IsBackButtonsEnabled(); + bool leftTrackpad = m_controller->IsUseLeftTrackpad(); + + HMENU menu = CreatePopupMenu(); + + UINT toggleFlags = MF_STRING | (connected ? MF_ENABLED : MF_GRAYED); + AppendMenuW(menu, toggleFlags, IDM_TOGGLE, + gameModeOn ? L"Disable Steamless Mode" : L"Enable Steamless Mode"); + + AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); + + UINT trackpadFlags = MF_STRING | (trackpadOn ? MF_CHECKED : MF_UNCHECKED); + AppendMenuW(menu, trackpadFlags, IDM_TRACKPAD, L"Enable Trackpad Mouse"); + + UINT backFlags = MF_STRING | (backButtonsOn ? MF_CHECKED : MF_UNCHECKED); + AppendMenuW(menu, backFlags, IDM_BACKBUTTONS, L"Enable Back Buttons for Clicking"); + + UINT leftFlags = MF_STRING | (leftTrackpad ? MF_CHECKED : MF_UNCHECKED); + AppendMenuW(menu, leftFlags, IDM_LEFT_TRACKPAD, L"Use Left Trackpad Instead"); + + AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); + AppendMenuW(menu, MF_STRING, IDM_EXIT, L"Exit"); + + // SetForegroundWindow is required for the menu to dismiss on click-away. + SetForegroundWindow(m_hwnd); + + POINT pt; + GetCursorPos(&pt); + TrackPopupMenu(menu, TPM_RIGHTALIGN | TPM_BOTTOMALIGN | TPM_RIGHTBUTTON, + pt.x, pt.y, 0, m_hwnd, nullptr); + DestroyMenu(menu); +} diff --git a/src/app/TrayApp.h b/src/app/TrayApp.h new file mode 100644 index 0000000..e829b10 --- /dev/null +++ b/src/app/TrayApp.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include + +class ControllerManager; + +class TrayApp { +public: + TrayApp(); + ~TrayApp(); + + bool Init(HINSTANCE hInstance); + int Run(); + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + LRESULT HandleMessage(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + + void AddTrayIcon(); + void RemoveTrayIcon(); + void UpdateTrayIcon(bool connected, bool gameModeActive, bool vigemMissing = false); + void ShowViGEmBalloon(); + void ShowContextMenu(); + void LoadSettings(); + void SaveSettings(); + + HWND m_hwnd = nullptr; + HINSTANCE m_hInstance = nullptr; + UINT m_wmTaskbar = 0; + HICON m_iconOff = nullptr; + HICON m_iconOn = nullptr; + std::unique_ptr m_controller; + + static constexpr UINT IDM_TOGGLE = 1001; + static constexpr UINT IDM_EXIT = 1002; + static constexpr UINT IDM_TRACKPAD = 1003; + static constexpr UINT IDM_BACKBUTTONS = 1004; + static constexpr UINT IDM_LEFT_TRACKPAD = 1005; + static constexpr UINT WM_TRAY = WM_APP + 1; + static constexpr UINT TRAY_UID = 1; +}; diff --git a/src/app/VirtualController.cpp b/src/app/VirtualController.cpp new file mode 100644 index 0000000..1b9171a --- /dev/null +++ b/src/app/VirtualController.cpp @@ -0,0 +1,113 @@ +#include "VirtualController.h" +#include "steam/SteamController.h" +#include +#include +#include + +// --------------------------------------------------------------------------- +// Report translation — 0x42 → XUSB_REPORT +// --------------------------------------------------------------------------- + +static XUSB_REPORT Translate(const uint8_t* buf, size_t n) { + XUSB_REPORT r{}; + if (n < 18) return r; + + const uint8_t b0 = buf[2]; + const uint8_t b1 = buf[3]; + const uint8_t b2 = buf[4]; + + // Face buttons + if (b0 & SteamController::BTN_A) r.wButtons |= XUSB_GAMEPAD_A; + if (b0 & SteamController::BTN_B) r.wButtons |= XUSB_GAMEPAD_B; + if (b0 & SteamController::BTN_X) r.wButtons |= XUSB_GAMEPAD_X; + if (b0 & SteamController::BTN_Y) r.wButtons |= XUSB_GAMEPAD_Y; + + // Bumpers + if (b2 & SteamController::BTN_LB) r.wButtons |= XUSB_GAMEPAD_LEFT_SHOULDER; + if (b1 & SteamController::BTN_RB) r.wButtons |= XUSB_GAMEPAD_RIGHT_SHOULDER; + + // Menu / View (Start / Back) + if (b0 & SteamController::BTN_MENU) r.wButtons |= XUSB_GAMEPAD_START; + if (b1 & SteamController::BTN_VIEW) r.wButtons |= XUSB_GAMEPAD_BACK; + + // Stick clicks + if (b1 & SteamController::BTN_LS) r.wButtons |= XUSB_GAMEPAD_LEFT_THUMB; + if (b0 & SteamController::BTN_RS) r.wButtons |= XUSB_GAMEPAD_RIGHT_THUMB; + + // Steam / Guide button + if (b2 & SteamController::BTN_STEAM) r.wButtons |= XUSB_GAMEPAD_GUIDE; + + // D-pad + if (b1 & SteamController::BTN_DPAD_UP) r.wButtons |= XUSB_GAMEPAD_DPAD_UP; + if (b1 & SteamController::BTN_DPAD_DN) r.wButtons |= XUSB_GAMEPAD_DPAD_DOWN; + if (b1 & SteamController::BTN_DPAD_LT) r.wButtons |= XUSB_GAMEPAD_DPAD_LEFT; + if (b1 & SteamController::BTN_DPAD_RT) r.wButtons |= XUSB_GAMEPAD_DPAD_RIGHT; + + // Triggers: 16-bit signed (0x0000–0x7FFF) → 8-bit (0–255) + int16_t ltRaw, rtRaw; + memcpy(<Raw, buf + 6, 2); + memcpy(&rtRaw, buf + 8, 2); + r.bLeftTrigger = static_cast(std::clamp(ltRaw >> 7, 0, 255)); + r.bRightTrigger = static_cast(std::clamp(rtRaw >> 7, 0, 255)); + + // Sticks: 16-bit signed, same range as XInput — pass through directly + memcpy(&r.sThumbLX, buf + 10, 2); + memcpy(&r.sThumbLY, buf + 12, 2); + memcpy(&r.sThumbRX, buf + 14, 2); + memcpy(&r.sThumbRY, buf + 16, 2); + + return r; +} + +// --------------------------------------------------------------------------- +// VirtualController +// --------------------------------------------------------------------------- + +VirtualController::VirtualController() { + m_client = vigem_alloc(); + if (!m_client) { printf("[ViGEm] alloc failed\n"); return; } + + VIGEM_ERROR err = vigem_connect(static_cast(m_client)); + if (!VIGEM_SUCCESS(err)) { + if (err == VIGEM_ERROR_BUS_NOT_FOUND || err == VIGEM_ERROR_BUS_ACCESS_FAILED) + m_driverMissing = true; + vigem_free(static_cast(m_client)); + m_client = nullptr; + return; + } + + m_target = vigem_target_x360_alloc(); + if (!m_target) { printf("[ViGEm] target alloc failed\n"); return; } + + err = vigem_target_add(static_cast(m_client), + static_cast(m_target)); + if (!VIGEM_SUCCESS(err)) { + printf("[ViGEm] target_add failed: 0x%08X\n", err); + vigem_target_free(static_cast(m_target)); + m_target = nullptr; + return; + } + + printf("[ViGEm] Virtual Xbox 360 controller connected\n"); + m_valid = true; +} + +VirtualController::~VirtualController() { + if (m_client && m_target) { + vigem_target_remove(static_cast(m_client), + static_cast(m_target)); + } + if (m_target) vigem_target_free(static_cast(m_target)); + if (m_client) { + vigem_disconnect(static_cast(m_client)); + vigem_free(static_cast(m_client)); + } +} + +void VirtualController::Update(const uint8_t* buf, size_t n) { + if (!m_valid) return; + XUSB_REPORT report = Translate(buf, n); + vigem_target_x360_update(static_cast(m_client), + static_cast(m_target), + report); +} diff --git a/src/app/VirtualController.h b/src/app/VirtualController.h new file mode 100644 index 0000000..04840fc --- /dev/null +++ b/src/app/VirtualController.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include + +class VirtualController { +public: + VirtualController(); + ~VirtualController(); + VirtualController(const VirtualController&) = delete; + VirtualController& operator=(const VirtualController&) = delete; + + bool IsValid() const { return m_valid; } + bool IsDriverMissing() const { return m_driverMissing; } + + void Update(const uint8_t* buf, size_t n); + +private: + void* m_client = nullptr; + void* m_target = nullptr; + bool m_valid = false; + bool m_driverMissing = false; +}; diff --git a/src/app/main.cpp b/src/app/main.cpp new file mode 100644 index 0000000..d37d521 --- /dev/null +++ b/src/app/main.cpp @@ -0,0 +1,19 @@ +#include "TrayApp.h" +#include + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) { + // Prevent multiple instances. + HANDLE mutex = CreateMutexW(nullptr, TRUE, L"SteamlessController_SingleInstance"); + if (!mutex || GetLastError() == ERROR_ALREADY_EXISTS) { + if (mutex) CloseHandle(mutex); + return 0; + } + + TrayApp app; + int result = 0; + if (app.Init(hInstance)) + result = app.Run(); + + CloseHandle(mutex); + return result; +} diff --git a/src/hid/HidDevice.cpp b/src/hid/HidDevice.cpp new file mode 100644 index 0000000..cd25926 --- /dev/null +++ b/src/hid/HidDevice.cpp @@ -0,0 +1,210 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include "HidDevice.h" + +// --------------------------------------------------------------------------- +// Enumeration +// --------------------------------------------------------------------------- + +std::vector HidDevice::Enumerate(uint16_t vid, uint16_t pid, uint16_t usagePage) { + GUID hidGuid; + HidD_GetHidGuid(&hidGuid); + + HDEVINFO devInfo = SetupDiGetClassDevsW(&hidGuid, nullptr, nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (devInfo == INVALID_HANDLE_VALUE) + return {}; + + std::vector paths; + SP_DEVICE_INTERFACE_DATA ifaceData{}; + ifaceData.cbSize = sizeof(ifaceData); + + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(devInfo, nullptr, &hidGuid, i, &ifaceData); ++i) { + DWORD needed = 0; + SetupDiGetDeviceInterfaceDetailW(devInfo, &ifaceData, nullptr, 0, &needed, nullptr); + if (needed == 0) + continue; + + auto detailBuf = std::make_unique(needed); + auto* detail = reinterpret_cast(detailBuf.get()); + detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + + if (!SetupDiGetDeviceInterfaceDetailW(devInfo, &ifaceData, detail, needed, nullptr, nullptr)) + continue; + + // Open with no access just to query attributes — avoids needing exclusive open. + HANDLE h = CreateFileW(detail->DevicePath, 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (h == INVALID_HANDLE_VALUE) + continue; + + HIDD_ATTRIBUTES attrs{}; + attrs.Size = sizeof(attrs); + bool match = HidD_GetAttributes(h, &attrs) + && attrs.VendorID == vid + && attrs.ProductID == pid; + + if (match && usagePage != 0) { + PHIDP_PREPARSED_DATA preparsed; + if (HidD_GetPreparsedData(h, &preparsed)) { + HIDP_CAPS caps{}; + if (HidP_GetCaps(preparsed, &caps) == HIDP_STATUS_SUCCESS) + match = (caps.UsagePage == usagePage); + HidD_FreePreparsedData(preparsed); + } else { + match = false; + } + } + + if (match) + paths.push_back(detail->DevicePath); + + CloseHandle(h); + } + + SetupDiDestroyDeviceInfoList(devInfo); + return paths; +} + +// --------------------------------------------------------------------------- +// Move semantics +// --------------------------------------------------------------------------- + +HidDevice::HidDevice(HidDevice&& o) noexcept + : m_handle(o.m_handle), m_event(o.m_event), m_outputReportLen(o.m_outputReportLen) { + o.m_handle = INVALID_HANDLE_VALUE; + o.m_event = INVALID_HANDLE_VALUE; +} + +HidDevice& HidDevice::operator=(HidDevice&& o) noexcept { + if (this != &o) { + Close(); + m_handle = o.m_handle; + m_event = o.m_event; + m_outputReportLen = o.m_outputReportLen; + o.m_handle = INVALID_HANDLE_VALUE; + o.m_event = INVALID_HANDLE_VALUE; + } + return *this; +} + +// --------------------------------------------------------------------------- +// Open / Close +// --------------------------------------------------------------------------- + +bool HidDevice::Open(const std::wstring& path) { + Close(); + + // FILE_SHARE_READ | FILE_SHARE_WRITE lets us coexist with Steam's open handle. + // Remove the share flags for exclusive access (Steam must be closed first). + m_handle = CreateFileW(path.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + nullptr); + + if (m_handle == INVALID_HANDLE_VALUE) { + wprintf(L"CreateFileW failed for %s: error %lu\n", path.c_str(), GetLastError()); + return false; + } + + m_event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (m_event == INVALID_HANDLE_VALUE) { + CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + return false; + } + + // Query report sizes. HidD_SetOutputReport and HidD_SetFeature each require + // their buffer to be exactly the maximum report length of their respective type. + PHIDP_PREPARSED_DATA preparsed; + if (HidD_GetPreparsedData(m_handle, &preparsed)) { + HIDP_CAPS caps{}; + if (HidP_GetCaps(preparsed, &caps) == HIDP_STATUS_SUCCESS) { + if (caps.OutputReportByteLength > 0) m_outputReportLen = caps.OutputReportByteLength; + if (caps.FeatureReportByteLength > 0) m_featureReportLen = caps.FeatureReportByteLength; + } + HidD_FreePreparsedData(preparsed); + } + return true; +} + +void HidDevice::Close() { + if (m_handle != INVALID_HANDLE_VALUE) { + CancelIo(m_handle); + CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } + if (m_event != INVALID_HANDLE_VALUE) { + CloseHandle(m_event); + m_event = INVALID_HANDLE_VALUE; + } +} + +// --------------------------------------------------------------------------- +// I/O +// --------------------------------------------------------------------------- + +bool HidDevice::SendOutputReport(const uint8_t* data, size_t size) { + // HidD_SetOutputReport requires the buffer to be exactly OutputReportByteLength. + // Pad with zeros if the caller provided a shorter buffer (e.g. a 7-byte report + // when the device's max output report is 64 bytes). + if (size < m_outputReportLen) { + std::vector padded(m_outputReportLen, 0); + std::memcpy(padded.data(), data, size); + BOOLEAN ok = HidD_SetOutputReport(m_handle, padded.data(), m_outputReportLen); + if (!ok) + printf("SendOutputReport(0x%02X) failed: error %lu\n", data[0], GetLastError()); + return ok == TRUE; + } + + BOOLEAN ok = HidD_SetOutputReport(m_handle, + const_cast(static_cast(data)), + static_cast(size)); + if (!ok) + printf("SendOutputReport(0x%02X) failed: error %lu\n", data[0], GetLastError()); + return ok == TRUE; +} + +bool HidDevice::SendFeatureReport(const uint8_t* data, size_t size) { + std::vector buf(m_featureReportLen, 0); + size_t copyLen = size < m_featureReportLen ? size : m_featureReportLen; + std::memcpy(buf.data(), data, copyLen); + BOOLEAN ok = HidD_SetFeature(m_handle, buf.data(), m_featureReportLen); + if (!ok) + printf("SendFeatureReport(reportId=0x%02X cmd=0x%02X) failed: error %lu\n", + buf[0], buf[1], GetLastError()); + return ok == TRUE; +} + +size_t HidDevice::ReadInputReport(uint8_t* buffer, size_t size, uint32_t timeoutMs) { + OVERLAPPED ov{}; + ov.hEvent = m_event; + ResetEvent(m_event); + + DWORD bytesRead = 0; + if (!ReadFile(m_handle, buffer, static_cast(size), &bytesRead, &ov)) { + if (GetLastError() != ERROR_IO_PENDING) + return 0; + + DWORD wait = WaitForSingleObject(m_event, timeoutMs); + if (wait != WAIT_OBJECT_0) { + CancelIo(m_handle); + return 0; + } + if (!GetOverlappedResult(m_handle, &ov, &bytesRead, FALSE)) + return 0; + } + + return static_cast(bytesRead); +} diff --git a/src/hid/HidDevice.h b/src/hid/HidDevice.h new file mode 100644 index 0000000..792a09f --- /dev/null +++ b/src/hid/HidDevice.h @@ -0,0 +1,47 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +class HidDevice { +public: + // Returns device paths for all HID interfaces matching vid/pid/usagePage. + // Pass usagePage=0 to return all matching interfaces. + static std::vector Enumerate(uint16_t vid, uint16_t pid, uint16_t usagePage = 0); + + HidDevice() = default; + ~HidDevice() { Close(); } + HidDevice(const HidDevice&) = delete; + HidDevice& operator=(const HidDevice&) = delete; + HidDevice(HidDevice&& o) noexcept; + HidDevice& operator=(HidDevice&& o) noexcept; + + // Open with exclusive write access. Returns false if device is in use or not found. + bool Open(const std::wstring& path); + void Close(); + bool IsOpen() const { return m_handle != INVALID_HANDLE_VALUE; } + + // Send a HID output report (interrupt OUT / SET_REPORT Output type). + // data[0] must be the report ID. Padded to OutputReportByteLength automatically. + bool SendOutputReport(const uint8_t* data, size_t size); + + // Send a HID feature report (SET_REPORT Feature type via EP0 control pipe). + // data[0] must be the feature report ID. Padded to FeatureReportByteLength automatically. + // This is the command channel the original Steam Controller used for all firmware commands. + bool SendFeatureReport(const uint8_t* data, size_t size); + + ULONG OutputReportByteLength() const { return m_outputReportLen; } + ULONG FeatureReportByteLength() const { return m_featureReportLen; } + + // Read the next HID input report. buffer[0] will be the report ID on return. + // Returns bytes read, or 0 on timeout/error. + size_t ReadInputReport(uint8_t* buffer, size_t size, uint32_t timeoutMs = 1000); + +private: + HANDLE m_handle = INVALID_HANDLE_VALUE; + HANDLE m_event = INVALID_HANDLE_VALUE; + ULONG m_outputReportLen = 64; + ULONG m_featureReportLen = 64; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5eef4e2 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include "steam/SteamController.h" + +static SteamController* g_controller = nullptr; + +static void OnSignal(int) { + printf("\nCleaning up — re-enabling lizard mode...\n"); + if (g_controller) { + g_controller->EnableLizardMode(); + g_controller->Close(); + } + exit(0); +} + +// --------------------------------------------------------------------------- +// Calibration — collect idle frames to identify analog axis byte positions +// --------------------------------------------------------------------------- + +static constexpr int CALIB_FRAMES = 200; +static constexpr uint8_t AXIS_THRESHOLD = 0; // any idle movement → treat as axis + +// Force specific byte pairs through calibration suppression even if they move at idle. +// Set to true to pin those bytes as "not an axis" so they appear in diff output. +static constexpr bool FORCE_SHOW_BYTES_10_11 = false; // left joystick X +static constexpr bool FORCE_SHOW_BYTES_12_13 = false; // left joystick Y +static constexpr bool FORCE_SHOW_BYTES_14_15 = false; // right joystick Y +static constexpr bool FORCE_SHOW_BYTES_16_17 = true; // right joystick X + +struct ByteStats { + uint8_t lo = 0xFF; + uint8_t hi = 0x00; + bool seeded = false; + uint8_t range() const { return seeded ? (hi - lo) : 0; } + void feed(uint8_t v) { + if (!seeded) { lo = hi = v; seeded = true; return; } + if (v < lo) lo = v; + if (v > hi) hi = v; + } +}; + +static ByteStats s_stats[64]; +static bool s_isAxis[64] = {}; // true → suppress in diff output +static size_t s_reportLen = 0; + +static void RunCalibration(SteamController& ctrl) { + printf("--- Calibration: leave controller idle, collecting %d frames ---\n", + CALIB_FRAMES); + + uint8_t buf[64]; + int collected = 0; + while (collected < CALIB_FRAMES) { + size_t n = ctrl.ReadReport(buf, sizeof(buf), 200); + if (n == 0 || buf[0] != SteamController::REPORT_STATE) + continue; + if (s_reportLen == 0) s_reportLen = n; + for (size_t i = 1; i < n; ++i) + s_stats[i].feed(buf[i]); + ++collected; + if (collected % 50 == 0) + printf(" %d / %d\n", collected, CALIB_FRAMES); + } + + // Apply force-show overrides before marking axes. + auto forceShow = [](size_t i) -> bool { + if (FORCE_SHOW_BYTES_10_11 && (i == 10 || i == 11)) return true; + if (FORCE_SHOW_BYTES_12_13 && (i == 12 || i == 13)) return true; + if (FORCE_SHOW_BYTES_14_15 && (i == 14 || i == 15)) return true; + if (FORCE_SHOW_BYTES_16_17 && (i == 16 || i == 17)) return true; + return false; + }; + + printf("\n--- Calibration complete. Byte ranges (idle): ---\n"); + printf(" [idx] lo hi range class\n"); + for (size_t i = 1; i < s_reportLen; ++i) { + const char* cls = ""; + if (forceShow(i)) { + s_isAxis[i] = false; + cls = " [forced]"; + } else if (s_stats[i].range() > AXIS_THRESHOLD) { + s_isAxis[i] = true; + cls = " "; + } else if (s_stats[i].hi == 0) { + cls = " (zero)"; + } else { + cls = " [candidate]"; + } + printf(" [%02zu] %02X %02X %3u %s\n", + i, s_stats[i].lo, s_stats[i].hi, s_stats[i].range(), cls); + } + printf("\nAxes suppressed. Press buttons one at a time — only stable bytes shown.\n\n"); +} + +// --------------------------------------------------------------------------- +// Differential display — axes masked, only changes shown +// --------------------------------------------------------------------------- + +static uint8_t s_prev42[64]; +static bool s_hasPrev42 = false; + +static void PrintState42Diff(const uint8_t* buf, size_t n) { + if (n < 2) return; + + bool anyDiff = false; + for (size_t i = 1; i < n; ++i) { + if (s_isAxis[i]) continue; + bool changed = !s_hasPrev42 || (buf[i] != s_prev42[i]); + if (changed) { + if (!anyDiff) { printf(" [idx] new old\n"); anyDiff = true; } + printf(" [%02zu] %02X (%02X)\n", i, buf[i], s_hasPrev42 ? s_prev42[i] : 0); + } + } + + memcpy(s_prev42, buf, n); + s_hasPrev42 = true; +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +int main() { + signal(SIGINT, OnSignal); + signal(SIGTERM, OnSignal); + + printf("=== SteamProbe — 2026 Steam Controller (VID=28DE PID=1302) ===\n\n"); + printf("NOTE: Close Steam before running this tool.\n\n"); + + SteamController controller; + g_controller = &controller; + + if (!controller.Open()) { + fprintf(stderr, "Could not open device.\n"); + return 1; + } + + printf("Disabling lizard mode...\n"); + if (!controller.DisableLizardMode()) { + fprintf(stderr, "Lizard mode disable failed.\n"); + return 1; + } + printf("Lizard mode OFF.\n\n"); + + RunCalibration(controller); + + uint8_t buf[64]; + while (true) { + size_t n = controller.ReadReport(buf, sizeof(buf), 100); + if (n == 0) continue; + + uint8_t reportId = buf[0]; + if (reportId == SteamController::REPORT_STATE) { + PrintState42Diff(buf, n); + } else { + printf("[0x%02X %zu bytes] ", reportId, n); + for (size_t i = 0; i < n; ++i) printf("%02X ", buf[i]); + printf("\n"); + } + } + + return 0; +} diff --git a/src/probe/RawControllerProbe.cpp b/src/probe/RawControllerProbe.cpp new file mode 100644 index 0000000..ad78d7a --- /dev/null +++ b/src/probe/RawControllerProbe.cpp @@ -0,0 +1,102 @@ +#define NOMINMAX +#include +#include +#include +#include +#include +#include +#include + +using namespace winrt; +using namespace Windows::Gaming::Input; + +static constexpr uint16_t VALVE_VID = 0x28DE; +static constexpr uint16_t SC_PID = 0x1302; + +int main() { + init_apartment(); + + printf("=== RawGameController Probe ===\n"); + printf("Close Steam before running. Enable Game Mode in the tray app first.\n\n"); + printf("Enumerating (waiting 1s for OS enumeration)...\n"); + Sleep(1000); + + auto controllers = RawGameController::RawGameControllers(); + + if (controllers.Size() == 0) { + printf("No RawGameController devices found.\n"); + return 0; + } + + printf("All detected controllers:\n"); + RawGameController target = nullptr; + for (auto const& ctrl : controllers) { + auto name = to_string(ctrl.DisplayName()); + printf(" \"%s\" VID=%04X PID=%04X Buttons=%u Axes=%u Switches=%u\n", + name.c_str(), + ctrl.HardwareVendorId(), ctrl.HardwareProductId(), + ctrl.ButtonCount(), ctrl.AxisCount(), ctrl.SwitchCount()); + + if (ctrl.HardwareVendorId() == VALVE_VID && ctrl.HardwareProductId() == SC_PID) + target = ctrl; + } + + printf("\n"); + + if (!target) { + printf("Steam Controller (VID=28DE PID=1302) not found.\n"); + printf("The OS may not be exposing the vendor HID collection to RawGameController.\n"); + printf("Registry mapping approach will NOT work.\n"); + return 1; + } + + uint32_t btnCount = target.ButtonCount(); + uint32_t axisCount = target.AxisCount(); + uint32_t switchCount = target.SwitchCount(); + + printf("Steam Controller found!\n"); + printf(" Buttons : %u\n", btnCount); + printf(" Axes : %u\n", axisCount); + printf(" Switches: %u\n\n", switchCount); + + if (btnCount == 0 && axisCount == 0 && switchCount == 0) { + printf("Device has no enumerable inputs.\n"); + printf("Registry mapping approach will NOT work.\n"); + return 1; + } + + printf("Registry mapping approach looks viable — polling live values.\n"); + printf("Move sticks, press buttons. Press Ctrl+C to exit.\n\n"); + + // vector is a bitset without .data(); use a heap bool array instead. + auto buttons = std::make_unique(btnCount ? btnCount : 1); + auto prevButtons= std::make_unique(btnCount ? btnCount : 1); + std::vector axes(axisCount, 0.0); + std::vector switches(switchCount ? switchCount : 1); + std::vector prevAxes(axisCount, -1.0); + for (uint32_t i = 0; i < btnCount; i++) { buttons[i] = false; prevButtons[i] = false; } + + while (true) { + // API order: buttons, switches, axes + target.GetCurrentReading( + winrt::array_view(buttons.get(), buttons.get() + btnCount), + winrt::array_view(switches.data(), switches.data() + switchCount), + winrt::array_view(axes.data(), axes.data() + axisCount)); + + for (uint32_t i = 0; i < axisCount; i++) { + if (std::abs(axes[i] - prevAxes[i]) > 0.001) { + printf(" Axis[%02u] = %.4f\n", i, axes[i]); + prevAxes[i] = axes[i]; + } + } + + for (uint32_t i = 0; i < btnCount; i++) { + if (buttons[i] != prevButtons[i]) { + printf(" Button[%02u] = %s\n", i, buttons[i] ? "PRESSED" : "released"); + prevButtons[i] = buttons[i]; + } + } + + Sleep(16); + } +} diff --git a/src/steam/SteamController.cpp b/src/steam/SteamController.cpp new file mode 100644 index 0000000..322848c --- /dev/null +++ b/src/steam/SteamController.cpp @@ -0,0 +1,116 @@ +#include "SteamController.h" +#include +#include +#include + +// --------------------------------------------------------------------------- +// Internal helper: build a 64-byte feature report command buffer. +// +// The 2026 Steam Controller routes firmware commands through Feature Report +// 0x01 (same channel the original SC used, now with an explicit report ID). +// +// Buffer layout: +// [0] FEATURE_REPORT_CMD (0x01) — HID feature report ID +// [1] cmd — command byte (0x81, 0x87, etc.) +// [2] payloadSize — number of payload bytes that follow +// [3..3+payloadSize-1] payload — command arguments +// [rest] zeros +// --------------------------------------------------------------------------- + +static void BuildCmd(uint8_t (&buf)[64], uint8_t cmd, + const uint8_t* payload = nullptr, uint8_t payloadSize = 0) { + std::memset(buf, 0, 64); + buf[0] = SteamController::FEATURE_REPORT_CMD; + buf[1] = cmd; + buf[2] = payloadSize; + if (payload && payloadSize) + std::memcpy(buf + 3, payload, payloadSize); +} + +// --------------------------------------------------------------------------- +// Open / Close +// --------------------------------------------------------------------------- + +bool SteamController::Open() { + auto paths = HidDevice::Enumerate(VALVE_VID, SC2026_PID, VENDOR_USAGE_PAGE); + if (paths.empty()) { + printf("No Steam Controller found (VID=%04X PID=%04X UsagePage=%04X).\n", + VALVE_VID, SC2026_PID, VENDOR_USAGE_PAGE); + return false; + } + + printf("Found %zu interface(s). Opening: ", paths.size()); + wprintf(L"%s\n", paths[0].c_str()); + + return m_device.Open(paths[0]); +} + +void SteamController::Close() { + if (m_running.exchange(false) && m_heartbeat.joinable()) + m_heartbeat.join(); + m_device.Close(); +} + +// --------------------------------------------------------------------------- +// Lizard mode +// --------------------------------------------------------------------------- + +bool SteamController::DisableLizardMode() { + uint8_t buf[64]; + + // Step 1: CLEAR_DIGITAL_MAPPINGS — kills keyboard/mouse button emulation. + BuildCmd(buf, CMD_CLEAR_DIGITAL_MAPPINGS); + if (!m_device.SendFeatureReport(buf, sizeof(buf))) { + printf("Failed to send CLEAR_DIGITAL_MAPPINGS.\n"); + return false; + } + + // Step 2: SET_SETTINGS — set both trackpads to TRACKPAD_NONE. + // Payload: pairs of [setting_id, val_lo, val_hi]. + const uint8_t settingsPayload[] = { + SETTING_LEFT_TRACKPAD_MODE, 0x00, 0x00, + SETTING_RIGHT_TRACKPAD_MODE, 0x00, 0x00, + }; + BuildCmd(buf, CMD_SET_SETTINGS, settingsPayload, sizeof(settingsPayload)); + if (!m_device.SendFeatureReport(buf, sizeof(buf))) { + printf("Failed to send SET_SETTINGS_VALUES.\n"); + return false; + } + + if (!m_running.exchange(true)) + m_heartbeat = std::thread(&SteamController::HeartbeatLoop, this); + + return true; +} + +bool SteamController::EnableLizardMode() { + if (m_running.exchange(false) && m_heartbeat.joinable()) + m_heartbeat.join(); + + uint8_t buf[64]; + BuildCmd(buf, CMD_SET_DEFAULT_MAPPINGS); + return m_device.SendFeatureReport(buf, sizeof(buf)); +} + +// --------------------------------------------------------------------------- +// Input +// --------------------------------------------------------------------------- + +size_t SteamController::ReadReport(uint8_t* buffer, size_t size, uint32_t timeoutMs) { + return m_device.ReadInputReport(buffer, size, timeoutMs); +} + + +// --------------------------------------------------------------------------- +// Heartbeat +// --------------------------------------------------------------------------- + +void SteamController::HeartbeatLoop() { + uint8_t buf[64]; + BuildCmd(buf, CMD_CLEAR_DIGITAL_MAPPINGS); + + while (m_running.load()) { + m_device.SendFeatureReport(buf, sizeof(buf)); + std::this_thread::sleep_for(std::chrono::milliseconds(800)); + } +} diff --git a/src/steam/SteamController.h b/src/steam/SteamController.h new file mode 100644 index 0000000..29cb206 --- /dev/null +++ b/src/steam/SteamController.h @@ -0,0 +1,138 @@ +#pragma once +#include "hid/HidDevice.h" +#include +#include +#include + +class SteamController { +public: + static constexpr uint16_t VALVE_VID = 0x28DE; + static constexpr uint16_t SC2026_PID = 0x1302; + + // HID Usage Page for the vendor collection that carries all game input. + static constexpr uint16_t VENDOR_USAGE_PAGE = 0xFF00; + + // Input report IDs (device → host) + static constexpr uint8_t REPORT_STATE = 0x42; // 53 bytes: main controller state + static constexpr uint8_t REPORT_SECONDARY = 0x43; // 14 bytes: gyro / secondary state + static constexpr uint8_t REPORT_STATUS = 0x44; // 5 bytes: battery / connection + static constexpr uint8_t REPORT_EXTENDED = 0x45; // 45 bytes: extended sensor state + static constexpr uint8_t REPORT_UNKNOWN_7B = 0x7B; // 12 bytes: TBD + static constexpr uint8_t REPORT_UNKNOWN_79 = 0x79; // 1 byte: TBD + + // Feature report IDs — the command channel to the firmware. + // Commands are wrapped inside Feature Report 0x01 (or 0x02 as fallback). + // Buffer layout: [feature_report_id | cmd_byte | payload_size | payload...] + static constexpr uint8_t FEATURE_REPORT_CMD = 0x01; + static constexpr uint8_t FEATURE_REPORT_CMD2 = 0x02; // fallback if 0x01 fails + + // Command bytes (go in buffer[1] inside the feature report) + static constexpr uint8_t CMD_SET_DIGITAL_MAPPINGS = 0x80; + static constexpr uint8_t CMD_CLEAR_DIGITAL_MAPPINGS = 0x81; // ← lizard off + static constexpr uint8_t CMD_GET_DIGITAL_MAPPINGS = 0x82; + static constexpr uint8_t CMD_SET_DEFAULT_MAPPINGS = 0x85; // ← lizard on + static constexpr uint8_t CMD_SET_SETTINGS = 0x87; + static constexpr uint8_t CMD_GET_SETTINGS = 0x89; + + // Setting key IDs (go in the payload of CMD_SET_SETTINGS) + static constexpr uint8_t SETTING_RIGHT_TRACKPAD_MODE = 0x07; + static constexpr uint8_t SETTING_LEFT_TRACKPAD_MODE = 0x08; + static constexpr uint8_t TRACKPAD_NONE = 0x00; + + // --------------------------------------------------------------------------- + // Input report layout — 0x42 STATE report (buf[0] = 0x42) + // --------------------------------------------------------------------------- + + // buf[01] — 8-bit sequence counter (wraps 0xFF → 0x00) + + // buf[02] — button bitmask byte 0 + static constexpr uint8_t BTN_A = 0x01; // bit 0 + static constexpr uint8_t BTN_B = 0x02; // bit 1 + static constexpr uint8_t BTN_X = 0x04; // bit 2 + static constexpr uint8_t BTN_Y = 0x08; // bit 3 + // bit 4 (0x10): TBD + static constexpr uint8_t BTN_RS = 0x20; // bit 5 — right stick click + static constexpr uint8_t BTN_MENU = 0x40; // bit 6 — ≡ Menu / Start + static constexpr uint8_t BTN_R4 = 0x80; // bit 7 — back paddle R4 + + // buf[03] — button bitmask byte 1 + static constexpr uint8_t BTN_R5 = 0x01; // bit 0 — back paddle R5 + static constexpr uint8_t BTN_RB = 0x02; // bit 1 + static constexpr uint8_t BTN_DPAD_DN = 0x04; // bit 2 + static constexpr uint8_t BTN_DPAD_RT = 0x08; // bit 3 + static constexpr uint8_t BTN_DPAD_LT = 0x10; // bit 4 + static constexpr uint8_t BTN_DPAD_UP = 0x20; // bit 5 + static constexpr uint8_t BTN_VIEW = 0x40; // bit 6 — ⧉ View / Back + static constexpr uint8_t BTN_LS = 0x80; // bit 7 — left stick click + + // buf[04] — button bitmask byte 2 + static constexpr uint8_t BTN_STEAM = 0x01; // bit 0 — Steam / Guide + static constexpr uint8_t BTN_L4 = 0x02; // bit 1 — back paddle L4 + static constexpr uint8_t BTN_L5 = 0x04; // bit 2 — back paddle L5 + static constexpr uint8_t BTN_LB = 0x08; // bit 3 + static constexpr uint8_t BTN_RS_TOUCH = 0x10; // bit 4 — right stick capacitive touch + static constexpr uint8_t BTN_TP_RT = 0x20; // bit 5 — right trackpad active (touch or click) + // bit 6 (0x40): TBD — possibly right trackpad physical click (hard press only) + static constexpr uint8_t BTN_RT_FULL = 0x80; // bit 7 — right trigger fully pressed (digital threshold) + + // buf[05] — flags byte + static constexpr uint8_t BTN_LS_TOUCH = 0x01; // bit 0 — left stick capacitive touch + static constexpr uint8_t BTN_TP_LT = 0x02; // bit 1 — left trackpad active (touch or click) + static constexpr uint8_t BTN_TP_LT_CLICK = 0x04; // bit 2 — left trackpad hard press + // bit 3 (0x08): TBD + static constexpr uint8_t FLAG_GRIP_RT = 0x10; // bit 4 — right grip sensor active + static constexpr uint8_t FLAG_GRIP_LT = 0x20; // bit 5 — left grip sensor active + // other bits TBD + + // buf[06..07] — left trigger, 16-bit LE signed, 0x0000 (released) – 0x7FFF (full) + // buf[08..09] — right trigger, 16-bit LE signed, 0x0000 (released) – 0x7FFF (full) + + // buf[10..11] — left joystick X, 16-bit LE signed; center ≈ 0x0000, +0x7FFF = right, −0x8000 = left + // buf[12..13] — left joystick Y, 16-bit LE signed; center ≈ 0x0000, +0x7FFF = up, −0x8000 = down + // buf[14..15] — right joystick X, 16-bit LE signed; center ≈ 0x0000, +0x7FFF = right, −0x8000 = left + // buf[16..17] — right joystick Y, 16-bit LE signed; center ≈ 0x0000, +0x7FFF = up, −0x8000 = down + + // buf[18..19] — left trackpad X, 16-bit LE signed (0x0000 when not touching) + // buf[20..21] — left trackpad Y, 16-bit LE signed (0x0000 when not touching) + // buf[22..23] — left trackpad contact area, 16-bit LE (0 = no contact; higher = more area/pressure) + + // buf[24..25] — right trackpad X, 16-bit LE signed (0x0000 when not touching) + // buf[26..27] — right trackpad Y, 16-bit LE signed (0x0000 when not touching) + // buf[28..29] — right trackpad contact area, 16-bit LE (0 = no contact; higher = more area/pressure) + + // buf[30..31] — 0x03 0x46 constant (firmware info?) + // buf[32..39] — IMU quaternion (4× 16-bit LE, little-endian) + // buf[40..43] — unknown / always zero + // buf[44..45] — 0xFF 0xFF at full charge (battery level) + // buf[46..53] — gyro at rest (constant when stationary) + + // --------------------------------------------------------------------------- + + SteamController() = default; + ~SteamController() { Close(); } + SteamController(const SteamController&) = delete; + SteamController& operator=(const SteamController&) = delete; + + // Finds and opens the vendor HID interface. Returns false if not found. + bool Open(); + void Close(); + bool IsOpen() const { return m_device.IsOpen(); } + + // Two-step sequence: clears digital mappings + sets trackpads to NONE. + // Starts the background heartbeat thread on first call. + bool DisableLizardMode(); + + // Restores default mappings. Should be called before process exit. + bool EnableLizardMode(); + + // Read the next raw input report. buffer[0] = report ID on return. + // Returns 0 on timeout. + size_t ReadReport(uint8_t* buffer, size_t size, uint32_t timeoutMs = 16); + +private: + void HeartbeatLoop(); + + HidDevice m_device; + std::thread m_heartbeat; + std::atomic m_running{false}; +}; diff --git a/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/bug_report.md b/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0140e55 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior (example): +1. Start program '...' +2. Click on '....' +3. Plug in device '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System details (please complete the following information):** + - OS: [e.g. Windows 10 1803] + - Feeder software: [e.g. VDX, DS4Windows, ...] + - Driver Version: [e.g. 1.14.3.0] + +**Additional context** +Add any other context about the problem here. diff --git a/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/feature_request.md b/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..066b2d9 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/you-need-support.md b/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/you-need-support.md new file mode 100644 index 0000000..98baada --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/.github/ISSUE_TEMPLATE/you-need-support.md @@ -0,0 +1,7 @@ +--- +name: You need support +about: This is the wrong place to ask for support + +--- + +**Please don't abuse the issue tracker as a helpdesk!** We have a Discord server (linked in the README) where you can ask the lovely like-minded folks for assistance. Thank you for your compliance :smiley: diff --git a/third_party/ViGEmClient-1.16.18.0/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/third_party/ViGEmClient-1.16.18.0/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..be5d79c --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1 @@ +# TBD :smiley: diff --git a/third_party/ViGEmClient-1.16.18.0/.gitignore b/third_party/ViGEmClient-1.16.18.0/.gitignore new file mode 100644 index 0000000..76e53a4 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/.gitignore @@ -0,0 +1,45 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs/ViGEmBus/v15 +/build/bin/Release +/build/obj/Release +/lib +/packages +/sys/x64/Release +/x64/Release +/x64/Release (dynamic) +/x64/Release (static) +/.tmp +/lib/Release (static) +/Release +/sys/Release +/Release (static) +/build/bin/Debug +/build/obj/Debug +/bin +/sys/x64 +/x64 +*.user +/.vs/config +/sys/RCa21300 +/sys/RCb21300 +/build/*.dotsettings +/artifacts +/.vs/ViGEmBus/DesignTimeBuild +/build/obj +/.vs/ViGEmClient +/src/Release (static) +/src/x64/Debug (static) +/src/x64/Release (dynamic) +/src/x64/Release (static) +*.TMP +/src/Debug_LIB +/src/x64/Debug_LIB +/x64/Debug_LIB +/Debug_LIB +*.aps +/Release_LIB +/src/Release_LIB +/src/x64 diff --git a/third_party/ViGEmClient-1.16.18.0/.nuke b/third_party/ViGEmClient-1.16.18.0/.nuke new file mode 100644 index 0000000..a9c2c79 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/.nuke @@ -0,0 +1 @@ +ViGEmClient.sln \ No newline at end of file diff --git a/third_party/ViGEmClient-1.16.18.0/LICENSE b/third_party/ViGEmClient-1.16.18.0/LICENSE new file mode 100644 index 0000000..5353eb2 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Benjamin Höglinger-Stelzer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/ViGEmClient-1.16.18.0/README.md b/third_party/ViGEmClient-1.16.18.0/README.md new file mode 100644 index 0000000..fd54a2b --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/README.md @@ -0,0 +1,22 @@ +# ViGEm Client Native SDK + +Developer SDK for communication with `ViGEmBus`. + +[![Build status](https://ci.appveyor.com/api/projects/status/k806d3m2egjr0j56?svg=true)](https://ci.appveyor.com/project/nefarius/vigemclient) [![Discord](https://img.shields.io/discord/346756263763378176.svg)](https://discord.gg/QTJpBX5) [![Website](https://img.shields.io/website-up-down-green-red/https/vigem.org.svg?label=ViGEm.org)](https://vigem.org/) [![PayPal Donate](https://img.shields.io/badge/paypal-donate-blue.svg)]() [![Support on Patreon](https://img.shields.io/badge/patreon-donate-orange.svg)]() [![GitHub followers](https://img.shields.io/github/followers/nefarius.svg?style=social&label=Follow)](https://github.com/nefarius) [![Twitter Follow](https://img.shields.io/twitter/follow/nefariusmaximus.svg?style=social&label=Follow)](https://twitter.com/nefariusmaximus) + +## About + +TBD + +## How to build + +### Prerequisites + +- Visual Studio **2017** ([Community Edition](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) is just fine) +- [.NET Core SDK 2.1](https://www.microsoft.com/net/download/dotnet-core/2.1) (or greater, required for building only) + +You can either build directly within Visual Studio or in PowerShell by running the build script: + +```PowerShell +.\build.ps1 -configuration release +``` diff --git a/third_party/ViGEmClient-1.16.18.0/ViGEmClient.sln b/third_party/ViGEmClient-1.16.18.0/ViGEmClient.sln new file mode 100644 index 0000000..8e022b6 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/ViGEmClient.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2024 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ViGEmClient", "src\ViGEmClient.vcxproj", "{7DB06674-1F4F-464B-8E1C-172E9587F9DC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{A11CD588-F74A-41B5-9E5B-E60937052009}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug_DLL|x64 = Debug_DLL|x64 + Debug_DLL|x86 = Debug_DLL|x86 + Debug_LIB|x64 = Debug_LIB|x64 + Debug_LIB|x86 = Debug_LIB|x86 + Release_DLL|x64 = Release_DLL|x64 + Release_DLL|x86 = Release_DLL|x86 + Release_LIB|x64 = Release_LIB|x64 + Release_LIB|x86 = Release_LIB|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_DLL|x64.ActiveCfg = Debug_DLL|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_DLL|x64.Build.0 = Debug_DLL|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_DLL|x86.ActiveCfg = Debug_DLL|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_DLL|x86.Build.0 = Debug_DLL|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_LIB|x64.ActiveCfg = Debug_LIB|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_LIB|x64.Build.0 = Debug_LIB|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_LIB|x86.ActiveCfg = Debug_LIB|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Debug_LIB|x86.Build.0 = Debug_LIB|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_DLL|x64.ActiveCfg = Release_DLL|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_DLL|x64.Build.0 = Release_DLL|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_DLL|x86.ActiveCfg = Release_DLL|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_DLL|x86.Build.0 = Release_DLL|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_LIB|x64.ActiveCfg = Release_LIB|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_LIB|x64.Build.0 = Release_LIB|x64 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_LIB|x86.ActiveCfg = Release_LIB|Win32 + {7DB06674-1F4F-464B-8E1C-172E9587F9DC}.Release_LIB|x86.Build.0 = Release_LIB|Win32 + {A11CD588-F74A-41B5-9E5B-E60937052009}.Debug_DLL|x64.ActiveCfg = Debug|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Debug_DLL|x86.ActiveCfg = Debug|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Debug_LIB|x64.ActiveCfg = Debug|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Debug_LIB|x86.ActiveCfg = Debug|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Release_DLL|x64.ActiveCfg = Release|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Release_DLL|x86.ActiveCfg = Release|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Release_LIB|x64.ActiveCfg = Release|Any CPU + {A11CD588-F74A-41B5-9E5B-E60937052009}.Release_LIB|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D5CD61FD-80BB-4E0E-840C-BAF66ABB1CF0} + EndGlobalSection +EndGlobal diff --git a/third_party/ViGEmClient-1.16.18.0/appveyor.yml b/third_party/ViGEmClient-1.16.18.0/appveyor.yml new file mode 100644 index 0000000..02f1487 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/appveyor.yml @@ -0,0 +1,9 @@ +version: 1.16.{build}.0 +image: Visual Studio 2017 +build_script: +- ps: .\build.ps1 -configuration Release_DLL +deploy: +- provider: Environment + name: BUILDBOT + on: + appveyor_repo_tag: true \ No newline at end of file diff --git a/third_party/ViGEmClient-1.16.18.0/build.ps1 b/third_party/ViGEmClient-1.16.18.0/build.ps1 new file mode 100644 index 0000000..728f31b --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/build.ps1 @@ -0,0 +1,65 @@ +[CmdletBinding()] +Param( + #[switch]$CustomParam, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "Windows PowerShell $($Host.Version)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { $host.SetShouldExit(1) } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.tmp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetReleasesUrl = "https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:NUGET_XMLDOC_MODE = "skip" + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If global.json exists, load expected version +if (Test-Path $DotNetGlobalFile) { + $DotNetVersion = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json).sdk.version +} + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and ` + (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + + # If expected version is not set, get latest version + if (!(Test-Path variable:DotNetVersion)) { + $DotNetVersion = $(Invoke-WebRequest -UseBasicParsing $DotNetReleasesUrl | ConvertFrom-Json)[0]."version-sdk" + } + + # Download and execute install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + md -force $TempDirectory > $null + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } +} + +Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" + +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } diff --git a/third_party/ViGEmClient-1.16.18.0/build.sh b/third_party/ViGEmClient-1.16.18.0/build.sh new file mode 100644 index 0000000..7dfb86b --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/build.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +echo $(bash --version 2>&1 | head -n 1) + +#CUSTOMPARAM=0 +BUILD_ARGUMENTS=() +for i in "$@"; do + case $(echo $1 | awk '{print tolower($0)}') in + # -custom-param) CUSTOMPARAM=1;; + *) BUILD_ARGUMENTS+=("$1") ;; + esac + shift +done + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" +DOTNET_RELEASES_URL="https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export NUGET_XMLDOC_MODE="skip" + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} +} + +# If global.json exists, load expected version +if [ -f "$DOTNET_GLOBAL_FILE" ]; then + DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) +fi + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then + export DOTNET_EXE="$(command -v dotnet)" +else + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + + # If expected version is not set, get latest version + if [ -z ${DOTNET_VERSION+x} ]; then + DOTNET_VERSION=$(FirstJsonValue "version-sdk" $(curl -s "$DOTNET_RELEASES_URL")) + fi + + # Download and execute install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path +fi + +echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/third_party/ViGEmClient-1.16.18.0/include/ViGEmClient.h b/third_party/ViGEmClient-1.16.18.0/include/ViGEmClient.h new file mode 100644 index 0000000..a0f9647 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/include/ViGEmClient.h @@ -0,0 +1,495 @@ +/* +MIT License + +Copyright (c) 2017 Benjamin "Nefarius" Höglinger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + + +#ifndef ViGEmClient_h__ +#define ViGEmClient_h__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ViGEmCommon.h" + +#ifdef VIGEM_DYNAMIC +#ifdef VIGEM_EXPORTS +#define VIGEM_API __declspec(dllexport) +#else +#define VIGEM_API __declspec(dllimport) +#endif +#else +#define VIGEM_API +#endif + + /** + * \typedef enum _VIGEM_ERRORS + * + * \brief Defines an alias representing the ViGEm errors. + */ + typedef enum _VIGEM_ERRORS + { + VIGEM_ERROR_NONE = 0x20000000, + VIGEM_ERROR_BUS_NOT_FOUND = 0xE0000001, + VIGEM_ERROR_NO_FREE_SLOT = 0xE0000002, + VIGEM_ERROR_INVALID_TARGET = 0xE0000003, + VIGEM_ERROR_REMOVAL_FAILED = 0xE0000004, + VIGEM_ERROR_ALREADY_CONNECTED = 0xE0000005, + VIGEM_ERROR_TARGET_UNINITIALIZED = 0xE0000006, + VIGEM_ERROR_TARGET_NOT_PLUGGED_IN = 0xE0000007, + VIGEM_ERROR_BUS_VERSION_MISMATCH = 0xE0000008, + VIGEM_ERROR_BUS_ACCESS_FAILED = 0xE0000009, + VIGEM_ERROR_CALLBACK_ALREADY_REGISTERED = 0xE0000010, + VIGEM_ERROR_CALLBACK_NOT_FOUND = 0xE0000011, + VIGEM_ERROR_BUS_ALREADY_CONNECTED = 0xE0000012, + VIGEM_ERROR_BUS_INVALID_HANDLE = 0xE0000013, + VIGEM_ERROR_XUSB_USERINDEX_OUT_OF_RANGE = 0xE0000014 + } VIGEM_ERROR; + + /** + * \def VIGEM_SUCCESS(_val_) (_val_ == VIGEM_ERROR_NONE); + * + * \brief A macro that defines success. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param _val_ The VIGEM_ERROR value. + */ +#define VIGEM_SUCCESS(_val_) (_val_ == VIGEM_ERROR_NONE) + + /** + * \typedef struct _VIGEM_CLIENT_T *PVIGEM_CLIENT + * + * \brief Defines an alias representing a driver connection object. + */ + typedef struct _VIGEM_CLIENT_T *PVIGEM_CLIENT; + + /** + * \typedef struct _VIGEM_TARGET_T *PVIGEM_TARGET + * + * \brief Defines an alias representing a target device object. + */ + typedef struct _VIGEM_TARGET_T *PVIGEM_TARGET; + + typedef + _Function_class_(EVT_VIGEM_TARGET_ADD_RESULT) + VOID + EVT_VIGEM_TARGET_ADD_RESULT( + PVIGEM_CLIENT Client, + PVIGEM_TARGET Target, + VIGEM_ERROR Result + ); + + typedef EVT_VIGEM_TARGET_ADD_RESULT *PFN_VIGEM_TARGET_ADD_RESULT; + + typedef + _Function_class_(EVT_VIGEM_X360_NOTIFICATION) + VOID + EVT_VIGEM_X360_NOTIFICATION( + PVIGEM_CLIENT Client, + PVIGEM_TARGET Target, + UCHAR LargeMotor, + UCHAR SmallMotor, + UCHAR LedNumber + ); + + typedef EVT_VIGEM_X360_NOTIFICATION *PFN_VIGEM_X360_NOTIFICATION; + + typedef + _Function_class_(EVT_VIGEM_DS4_NOTIFICATION) + VOID + EVT_VIGEM_DS4_NOTIFICATION( + PVIGEM_CLIENT Client, + PVIGEM_TARGET Target, + UCHAR LargeMotor, + UCHAR SmallMotor, + DS4_LIGHTBAR_COLOR LightbarColor + ); + + typedef EVT_VIGEM_DS4_NOTIFICATION *PFN_VIGEM_DS4_NOTIFICATION; + + /** + * \fn PVIGEM_CLIENT vigem_alloc(void); + * + * \brief Allocates an object representing a driver connection. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \return A new driver connection object. + */ + VIGEM_API PVIGEM_CLIENT vigem_alloc(void); + + /** + * \fn void vigem_free(PVIGEM_CLIENT vigem); + * + * \brief Frees up memory used by the driver connection object. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + */ + VIGEM_API void vigem_free(PVIGEM_CLIENT vigem); + + /** + * \fn VIGEM_ERROR vigem_connect(PVIGEM_CLIENT vigem); + * + * \brief Initializes the driver object and establishes a connection to the emulation bus + * driver. Returns an error if no compatible bus device has been found. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_connect(PVIGEM_CLIENT vigem); + + /** + * \fn void vigem_disconnect(PVIGEM_CLIENT vigem); + * + * \brief Disconnects from the bus device and resets the driver object state. The driver object + * may be reused again after calling this function. When called, all targets which may + * still be connected will be destroyed automatically. Be aware, that allocated target + * objects won't be automatically freed, this has to be taken care of by the caller. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + */ + VIGEM_API void vigem_disconnect(PVIGEM_CLIENT vigem); + + /** + * \fn PVIGEM_TARGET vigem_target_x360_alloc(void); + * + * \brief Allocates an object representing an Xbox 360 Controller device. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \return A PVIGEM_TARGET representing an Xbox 360 Controller device. + */ + VIGEM_API PVIGEM_TARGET vigem_target_x360_alloc(void); + + /** + * \fn PVIGEM_TARGET vigem_target_ds4_alloc(void); + * + * \brief Allocates an object representing a DualShock 4 Controller device. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \return A PVIGEM_TARGET representing a DualShock 4 Controller device. + */ + VIGEM_API PVIGEM_TARGET vigem_target_ds4_alloc(void); + + /** + * \fn void vigem_target_free(PVIGEM_TARGET target); + * + * \brief Frees up memory used by the target device object. This does not automatically remove + * the associated device from the bus, if present. If the target device doesn't get + * removed before this call, the device becomes orphaned until the owning process is + * terminated. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + */ + VIGEM_API void vigem_target_free(PVIGEM_TARGET target); + + /** + * \fn VIGEM_ERROR vigem_target_add(PVIGEM_CLIENT vigem, PVIGEM_TARGET target); + * + * \brief Adds a provided target device to the bus driver, which is equal to a device plug-in + * event of a physical hardware device. This function blocks until the target device is + * in full operational mode. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_add(PVIGEM_CLIENT vigem, PVIGEM_TARGET target); + + /** + * \fn VIGEM_ERROR vigem_target_add_async(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PVIGEM_TARGET_ADD_RESULT result); + * + * \brief Adds a provided target device to the bus driver, which is equal to a device plug-in + * event of a physical hardware device. This function immediately returns. An optional + * callback may be registered which gets called on error or if the target device has + * become fully operational. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * \param result An optional function getting called when the target device becomes available. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_add_async(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PFN_VIGEM_TARGET_ADD_RESULT result); + + /** + * \fn VIGEM_ERROR vigem_target_remove(PVIGEM_CLIENT vigem, PVIGEM_TARGET target); + * + * \brief Removes a provided target device from the bus driver, which is equal to a device + * unplug event of a physical hardware device. The target device object may be reused + * after this function is called. If this function is never called on target device + * objects, they will be removed from the bus when the owning process terminates. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_remove(PVIGEM_CLIENT vigem, PVIGEM_TARGET target); + + /** + * \fn VIGEM_ERROR vigem_target_x360_register_notification(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PVIGEM_X360_NOTIFICATION notification); + * + * \brief Registers a function which gets called, when LED index or vibration state changes + * occur on the provided target device. This function fails if the provided target + * device isn't fully operational or in an erroneous state. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * \param notification The notification callback. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_x360_register_notification(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PFN_VIGEM_X360_NOTIFICATION notification); + + /** + * \fn VIGEM_ERROR vigem_target_ds4_register_notification(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PVIGEM_DS4_NOTIFICATION notification); + * + * \brief Registers a function which gets called, when LightBar or vibration state changes + * occur on the provided target device. This function fails if the provided target + * device isn't fully operational or in an erroneous state. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * \param notification The notification callback. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_ds4_register_notification(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PFN_VIGEM_DS4_NOTIFICATION notification); + + /** + * \fn void vigem_target_x360_unregister_notification(PVIGEM_TARGET target); + * + * \brief Removes a previously registered callback function from the provided target object. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + */ + VIGEM_API void vigem_target_x360_unregister_notification(PVIGEM_TARGET target); + + /** + * \fn void vigem_target_ds4_unregister_notification(PVIGEM_TARGET target); + * + * \brief Removes a previously registered callback function from the provided target object. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + */ + VIGEM_API void vigem_target_ds4_unregister_notification(PVIGEM_TARGET target); + + /** + * \fn void vigem_target_set_vid(PVIGEM_TARGET target, USHORT vid); + * + * \brief Overrides the default Vendor ID value with the provided one. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + * \param vid The Vendor ID to set. + */ + VIGEM_API void vigem_target_set_vid(PVIGEM_TARGET target, USHORT vid); + + /** + * \fn void vigem_target_set_pid(PVIGEM_TARGET target, USHORT pid); + * + * \brief Overrides the default Product ID value with the provided one. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + * \param pid The Product ID to set. + */ + VIGEM_API void vigem_target_set_pid(PVIGEM_TARGET target, USHORT pid); + + /** + * \fn USHORT vigem_target_get_vid(PVIGEM_TARGET target); + * + * \brief Returns the Vendor ID of the provided target device object. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + * + * \return The Vendor ID. + */ + VIGEM_API USHORT vigem_target_get_vid(PVIGEM_TARGET target); + + /** + * \fn USHORT vigem_target_get_pid(PVIGEM_TARGET target); + * + * \brief Returns the Product ID of the provided target device object. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + * + * \return The Product ID. + */ + VIGEM_API USHORT vigem_target_get_pid(PVIGEM_TARGET target); + + /** + * \fn VIGEM_ERROR vigem_target_x360_update(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, XUSB_REPORT report); + * + * \brief Sends a state report to the provided target device. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * \param report The report to send to the target device. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_x360_update(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, XUSB_REPORT report); + + /** + * \fn VIGEM_ERROR vigem_target_ds4_update(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, DS4_REPORT report); + * + * \brief Sends a state report to the provided target device. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param vigem The driver connection object. + * \param target The target device object. + * \param report The report to send to the target device. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_ds4_update(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, DS4_REPORT report); + + /** + * \fn ULONG vigem_target_get_index(PVIGEM_TARGET target); + * + * \brief Returns the internal index (serial number) the bus driver assigned to the provided + * target device object. Note that this value is specific to the inner workings of the + * bus driver, it does not reflect related values like player index or device arrival + * order experienced by other APIs. It may be used to identify the target device object + * for its lifetime. This value becomes invalid once the target device is removed from + * the bus and may change on the next addition of the device. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + * + * \return The internally used index of the target device. + */ + VIGEM_API ULONG vigem_target_get_index(PVIGEM_TARGET target); + + /** + * \fn VIGEM_TARGET_TYPE vigem_target_get_type(PVIGEM_TARGET target); + * + * \brief Returns the type of the provided target device object. + * + * \author Benjamin "Nefarius" Höglinger + * \date 28.08.2017 + * + * \param target The target device object. + * + * \return A VIGEM_TARGET_TYPE. + */ + VIGEM_API VIGEM_TARGET_TYPE vigem_target_get_type(PVIGEM_TARGET target); + + /** + * \fn BOOL vigem_target_is_attached(PVIGEM_TARGET target); + * + * \brief Returns TRUE if the provided target device object is currently attached to the bus, + * FALSE otherwise. + * + * \author Benjamin "Nefarius" Höglinger + * \date 30.08.2017 + * + * \param target The target device object. + * + * \return TRUE if device is attached to the bus, FALSE otherwise. + */ + VIGEM_API BOOL vigem_target_is_attached(PVIGEM_TARGET target); + + /** + * \fn VIGEM_API VIGEM_ERROR vigem_target_x360_get_user_index(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PULONG index); + * + * \brief Returns the user index of the emulated Xenon device. This value correspondents to the + * (zero-based) index number representing the player number via LED present on a + * physical controller and is compatible to the dwUserIndex propery of the XInput* APIs. + * + * \author Benjamin "Nefarius" Höglinger + * \date 10.05.2018 + * + * \param vigem The driver connection object. + * \param target The target device object. + * \param index The (zero-based) user index of the Xenon device. + * + * \return A VIGEM_ERROR. + */ + VIGEM_API VIGEM_ERROR vigem_target_x360_get_user_index(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PULONG index); + +#ifdef __cplusplus +} +#endif + +#endif // ViGEmClient_h__ diff --git a/third_party/ViGEmClient-1.16.18.0/include/ViGEmCommon.h b/third_party/ViGEmClient-1.16.18.0/include/ViGEmCommon.h new file mode 100644 index 0000000..990b50e --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/include/ViGEmCommon.h @@ -0,0 +1,206 @@ +/* +MIT License + +Copyright (c) 2017 Benjamin "Nefarius" Höglinger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + + +#pragma once + +// +// Represents the desired target type for the emulated device. +// +typedef enum _VIGEM_TARGET_TYPE +{ + // + // Microsoft Xbox 360 Controller (wired) + // + Xbox360Wired, + // + // Microsoft Xbox One Controller (wired) + // + XboxOneWired, + // + // Sony DualShock 4 (wired) + // + DualShock4Wired + +} VIGEM_TARGET_TYPE, *PVIGEM_TARGET_TYPE; + +// +// Possible XUSB report buttons. +// +typedef enum _XUSB_BUTTON +{ + XUSB_GAMEPAD_DPAD_UP = 0x0001, + XUSB_GAMEPAD_DPAD_DOWN = 0x0002, + XUSB_GAMEPAD_DPAD_LEFT = 0x0004, + XUSB_GAMEPAD_DPAD_RIGHT = 0x0008, + XUSB_GAMEPAD_START = 0x0010, + XUSB_GAMEPAD_BACK = 0x0020, + XUSB_GAMEPAD_LEFT_THUMB = 0x0040, + XUSB_GAMEPAD_RIGHT_THUMB = 0x0080, + XUSB_GAMEPAD_LEFT_SHOULDER = 0x0100, + XUSB_GAMEPAD_RIGHT_SHOULDER = 0x0200, + XUSB_GAMEPAD_GUIDE = 0x0400, + XUSB_GAMEPAD_A = 0x1000, + XUSB_GAMEPAD_B = 0x2000, + XUSB_GAMEPAD_X = 0x4000, + XUSB_GAMEPAD_Y = 0x8000 + +} XUSB_BUTTON, *PXUSB_BUTTON; + +// +// Represents an XINPUT_GAMEPAD-compatible report structure. +// +typedef struct _XUSB_REPORT +{ + USHORT wButtons; + BYTE bLeftTrigger; + BYTE bRightTrigger; + SHORT sThumbLX; + SHORT sThumbLY; + SHORT sThumbRX; + SHORT sThumbRY; + +} XUSB_REPORT, *PXUSB_REPORT; + +// +// Initializes a _XUSB_REPORT structure. +// +VOID FORCEINLINE XUSB_REPORT_INIT( + _Out_ PXUSB_REPORT Report +) +{ + RtlZeroMemory(Report, sizeof(XUSB_REPORT)); +} + +// +// The color value (RGB) of a DualShock 4 Lightbar +// +typedef struct _DS4_LIGHTBAR_COLOR +{ + // + // Red part of the Lightbar (0-255). + // + UCHAR Red; + + // + // Green part of the Lightbar (0-255). + // + UCHAR Green; + + // + // Blue part of the Lightbar (0-255). + // + UCHAR Blue; + +} DS4_LIGHTBAR_COLOR, *PDS4_LIGHTBAR_COLOR; + +// +// DualShock 4 digital buttons +// +typedef enum _DS4_BUTTONS +{ + DS4_BUTTON_THUMB_RIGHT = 1 << 15, + DS4_BUTTON_THUMB_LEFT = 1 << 14, + DS4_BUTTON_OPTIONS = 1 << 13, + DS4_BUTTON_SHARE = 1 << 12, + DS4_BUTTON_TRIGGER_RIGHT = 1 << 11, + DS4_BUTTON_TRIGGER_LEFT = 1 << 10, + DS4_BUTTON_SHOULDER_RIGHT = 1 << 9, + DS4_BUTTON_SHOULDER_LEFT = 1 << 8, + DS4_BUTTON_TRIANGLE = 1 << 7, + DS4_BUTTON_CIRCLE = 1 << 6, + DS4_BUTTON_CROSS = 1 << 5, + DS4_BUTTON_SQUARE = 1 << 4 + +} DS4_BUTTONS, *PDS4_BUTTONS; + +// +// DualShock 4 special buttons +// +typedef enum _DS4_SPECIAL_BUTTONS +{ + DS4_SPECIAL_BUTTON_PS = 1 << 0, + DS4_SPECIAL_BUTTON_TOUCHPAD = 1 << 1 + +} DS4_SPECIAL_BUTTONS, *PDS4_SPECIAL_BUTTONS; + +// +// DualShock 4 directional pad (HAT) values +// +typedef enum _DS4_DPAD_DIRECTIONS +{ + DS4_BUTTON_DPAD_NONE = 0x8, + DS4_BUTTON_DPAD_NORTHWEST = 0x7, + DS4_BUTTON_DPAD_WEST = 0x6, + DS4_BUTTON_DPAD_SOUTHWEST = 0x5, + DS4_BUTTON_DPAD_SOUTH = 0x4, + DS4_BUTTON_DPAD_SOUTHEAST = 0x3, + DS4_BUTTON_DPAD_EAST = 0x2, + DS4_BUTTON_DPAD_NORTHEAST = 0x1, + DS4_BUTTON_DPAD_NORTH = 0x0 + +} DS4_DPAD_DIRECTIONS, *PDS4_DPAD_DIRECTIONS; + +// +// DualShock 4 HID Input report +// +typedef struct _DS4_REPORT +{ + BYTE bThumbLX; + BYTE bThumbLY; + BYTE bThumbRX; + BYTE bThumbRY; + USHORT wButtons; + BYTE bSpecial; + BYTE bTriggerL; + BYTE bTriggerR; + +} DS4_REPORT, *PDS4_REPORT; + +// +// Sets the current state of the D-PAD on a DualShock 4 report. +// +VOID FORCEINLINE DS4_SET_DPAD( + _Out_ PDS4_REPORT Report, + _In_ DS4_DPAD_DIRECTIONS Dpad +) +{ + Report->wButtons &= ~0xF; + Report->wButtons |= (USHORT)Dpad; +} + +VOID FORCEINLINE DS4_REPORT_INIT( + _Out_ PDS4_REPORT Report +) +{ + RtlZeroMemory(Report, sizeof(DS4_REPORT)); + + Report->bThumbLX = 0x80; + Report->bThumbLY = 0x80; + Report->bThumbRX = 0x80; + Report->bThumbRY = 0x80; + + DS4_SET_DPAD(Report, DS4_BUTTON_DPAD_NONE); +} + diff --git a/third_party/ViGEmClient-1.16.18.0/include/ViGEmUtil.h b/third_party/ViGEmClient-1.16.18.0/include/ViGEmUtil.h new file mode 100644 index 0000000..3233f15 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/include/ViGEmUtil.h @@ -0,0 +1,50 @@ +#pragma once + +#include "ViGEmCommon.h" +#include + +VOID FORCEINLINE XUSB_TO_DS4_REPORT( + _Out_ PXUSB_REPORT Input, + _Out_ PDS4_REPORT Output +) +{ + if (Input->wButtons & XUSB_GAMEPAD_BACK) Output->wButtons |= DS4_BUTTON_SHARE; + if (Input->wButtons & XUSB_GAMEPAD_START) Output->wButtons |= DS4_BUTTON_OPTIONS; + if (Input->wButtons & XUSB_GAMEPAD_LEFT_THUMB) Output->wButtons |= DS4_BUTTON_THUMB_LEFT; + if (Input->wButtons & XUSB_GAMEPAD_RIGHT_THUMB) Output->wButtons |= DS4_BUTTON_THUMB_RIGHT; + if (Input->wButtons & XUSB_GAMEPAD_LEFT_SHOULDER) Output->wButtons |= DS4_BUTTON_SHOULDER_LEFT; + if (Input->wButtons & XUSB_GAMEPAD_RIGHT_SHOULDER) Output->wButtons |= DS4_BUTTON_SHOULDER_RIGHT; + if (Input->wButtons & XUSB_GAMEPAD_GUIDE) Output->bSpecial |= DS4_SPECIAL_BUTTON_PS; + if (Input->wButtons & XUSB_GAMEPAD_A) Output->wButtons |= DS4_BUTTON_CROSS; + if (Input->wButtons & XUSB_GAMEPAD_B) Output->wButtons |= DS4_BUTTON_CIRCLE; + if (Input->wButtons & XUSB_GAMEPAD_X) Output->wButtons |= DS4_BUTTON_SQUARE; + if (Input->wButtons & XUSB_GAMEPAD_Y) Output->wButtons |= DS4_BUTTON_TRIANGLE; + + Output->bTriggerL = Input->bLeftTrigger; + Output->bTriggerR = Input->bRightTrigger; + + if (Input->bLeftTrigger > 0)Output->wButtons |= DS4_BUTTON_TRIGGER_LEFT; + if (Input->bRightTrigger > 0)Output->wButtons |= DS4_BUTTON_TRIGGER_RIGHT; + + if (Input->wButtons & XUSB_GAMEPAD_DPAD_UP) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_NORTH); + if (Input->wButtons & XUSB_GAMEPAD_DPAD_RIGHT) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_EAST); + if (Input->wButtons & XUSB_GAMEPAD_DPAD_DOWN) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_SOUTH); + if (Input->wButtons & XUSB_GAMEPAD_DPAD_LEFT) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_WEST); + + if (Input->wButtons & XUSB_GAMEPAD_DPAD_UP + && Input->wButtons & XUSB_GAMEPAD_DPAD_RIGHT) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_NORTHEAST); + if (Input->wButtons & XUSB_GAMEPAD_DPAD_RIGHT + && Input->wButtons & XUSB_GAMEPAD_DPAD_DOWN) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_SOUTHEAST); + if (Input->wButtons & XUSB_GAMEPAD_DPAD_DOWN + && Input->wButtons & XUSB_GAMEPAD_DPAD_LEFT) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_SOUTHWEST); + if (Input->wButtons & XUSB_GAMEPAD_DPAD_LEFT + && Input->wButtons & XUSB_GAMEPAD_DPAD_UP) DS4_SET_DPAD(Output, DS4_BUTTON_DPAD_NORTHWEST); + + Output->bThumbLX = ((Input->sThumbLX + ((USHRT_MAX / 2) + 1)) / 257); + Output->bThumbLY = (-(Input->sThumbLY + ((USHRT_MAX / 2) - 1)) / 257); + Output->bThumbLY = (Output->bThumbLY == 0) ? 0xFF : Output->bThumbLY; + Output->bThumbRX = ((Input->sThumbRX + ((USHRT_MAX / 2) + 1)) / 257); + Output->bThumbRY = (-(Input->sThumbRY + ((USHRT_MAX / 2) + 1)) / 257); + Output->bThumbRY = (Output->bThumbRY == 0) ? 0xFF : Output->bThumbRY; +} + diff --git a/third_party/ViGEmClient-1.16.18.0/include/km/ViGEmBusShared.h b/third_party/ViGEmClient-1.16.18.0/include/km/ViGEmBusShared.h new file mode 100644 index 0000000..941da48 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/include/km/ViGEmBusShared.h @@ -0,0 +1,493 @@ +/* +MIT License + +Copyright (c) 2016 Benjamin "Nefarius" Höglinger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + + +// {96E42B22-F5E9-42F8-B043-ED0F932F014F} +// ReSharper disable once CppMissingIncludeGuard +DEFINE_GUID(GUID_DEVINTERFACE_BUSENUM_VIGEM, + 0x96E42B22, 0xF5E9, 0x42F8, 0xB0, 0x43, 0xED, 0x0F, 0x93, 0x2F, 0x01, 0x4F); + +#pragma once + +#include "ViGEmCommon.h" + +// +// Common version for user-mode library and driver compatibility +// +// On initialization, the user-mode library has this number embedded +// and sends it to the bus on its enumeration. The bus compares this +// number to the one it was compiled with. If they match, the bus +// access is permitted and success reported. If they mismatch, an +// error is reported and the user-mode library skips this instance. +// +#define VIGEM_COMMON_VERSION 0x0001 + +#define FILE_DEVICE_BUSENUM FILE_DEVICE_BUS_EXTENDER +#define BUSENUM_IOCTL(_index_) CTL_CODE(FILE_DEVICE_BUSENUM, _index_, METHOD_BUFFERED, FILE_READ_DATA) +#define BUSENUM_W_IOCTL(_index_) CTL_CODE(FILE_DEVICE_BUSENUM, _index_, METHOD_BUFFERED, FILE_WRITE_DATA) +#define BUSENUM_R_IOCTL(_index_) CTL_CODE(FILE_DEVICE_BUSENUM, _index_, METHOD_BUFFERED, FILE_READ_DATA) +#define BUSENUM_RW_IOCTL(_index_) CTL_CODE(FILE_DEVICE_BUSENUM, _index_, METHOD_BUFFERED, FILE_WRITE_DATA | FILE_READ_DATA) + +#define IOCTL_VIGEM_BASE 0x801 + +// +// IO control codes +// +#define IOCTL_VIGEM_PLUGIN_TARGET BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x000) +#define IOCTL_VIGEM_UNPLUG_TARGET BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x001) +#define IOCTL_VIGEM_CHECK_VERSION BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x002) + +#define IOCTL_XUSB_REQUEST_NOTIFICATION BUSENUM_RW_IOCTL(IOCTL_VIGEM_BASE + 0x200) +#define IOCTL_XUSB_SUBMIT_REPORT BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x201) +#define IOCTL_DS4_SUBMIT_REPORT BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x202) +#define IOCTL_DS4_REQUEST_NOTIFICATION BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x203) +#define IOCTL_XGIP_SUBMIT_REPORT BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x204) +#define IOCTL_XGIP_SUBMIT_INTERRUPT BUSENUM_W_IOCTL (IOCTL_VIGEM_BASE + 0x205) +#define IOCTL_XUSB_GET_USER_INDEX BUSENUM_RW_IOCTL(IOCTL_VIGEM_BASE + 0x206) + + +// +// Data structure used in PlugIn and UnPlug ioctls +// + +#pragma region Plugin + +// +// Data structure used in IOCTL_VIGEM_PLUGIN_TARGET requests. +// +typedef struct _VIGEM_PLUGIN_TARGET +{ + // + // sizeof (struct _BUSENUM_HARDWARE) + // + IN ULONG Size; + + // + // Serial number of target device. + // + IN ULONG SerialNo; + + // + // Type of the target device to emulate. + // + VIGEM_TARGET_TYPE TargetType; + + // + // If set, the vendor ID the emulated device is reporting + // + USHORT VendorId; + + // + // If set, the product ID the emulated device is reporting + // + USHORT ProductId; + +} VIGEM_PLUGIN_TARGET, *PVIGEM_PLUGIN_TARGET; + +// +// Initializes a VIGEM_PLUGIN_TARGET structure. +// +VOID FORCEINLINE VIGEM_PLUGIN_TARGET_INIT( + _Out_ PVIGEM_PLUGIN_TARGET PlugIn, + _In_ ULONG SerialNo, + _In_ VIGEM_TARGET_TYPE TargetType +) +{ + RtlZeroMemory(PlugIn, sizeof(VIGEM_PLUGIN_TARGET)); + + PlugIn->Size = sizeof(VIGEM_PLUGIN_TARGET); + PlugIn->SerialNo = SerialNo; + PlugIn->TargetType = TargetType; +} + +#pragma endregion + +#pragma region Unplug + +// +// Data structure used in IOCTL_VIGEM_UNPLUG_TARGET requests. +// +typedef struct _VIGEM_UNPLUG_TARGET +{ + // + // sizeof (struct _REMOVE_HARDWARE) + // + IN ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + +} VIGEM_UNPLUG_TARGET, *PVIGEM_UNPLUG_TARGET; + +// +// Initializes a VIGEM_UNPLUG_TARGET structure. +// +VOID FORCEINLINE VIGEM_UNPLUG_TARGET_INIT( + _Out_ PVIGEM_UNPLUG_TARGET UnPlug, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(UnPlug, sizeof(VIGEM_UNPLUG_TARGET)); + + UnPlug->Size = sizeof(VIGEM_UNPLUG_TARGET); + UnPlug->SerialNo = SerialNo; +} + +#pragma endregion + +#pragma region Check version + +typedef struct _VIGEM_CHECK_VERSION +{ + IN ULONG Size; + + IN ULONG Version; + +} VIGEM_CHECK_VERSION, *PVIGEM_CHECK_VERSION; + +VOID FORCEINLINE VIGEM_CHECK_VERSION_INIT( + _Out_ PVIGEM_CHECK_VERSION CheckVersion, + _In_ ULONG Version +) +{ + RtlZeroMemory(CheckVersion, sizeof(VIGEM_CHECK_VERSION)); + + CheckVersion->Size = sizeof(VIGEM_CHECK_VERSION); + CheckVersion->Version = Version; +} + +#pragma endregion + +#pragma region XUSB (aka Xbox 360 device) section + +// +// Data structure used in IOCTL_XUSB_REQUEST_NOTIFICATION requests. +// +typedef struct _XUSB_REQUEST_NOTIFICATION +{ + // + // sizeof(struct _XUSB_REQUEST_NOTIFICATION) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // Vibration intensity value of the large motor (0-255). + // + UCHAR LargeMotor; + + // + // Vibration intensity value of the small motor (0-255). + // + UCHAR SmallMotor; + + // + // Index number of the slot/LED that XUSB.sys has assigned. + // + UCHAR LedNumber; + +} XUSB_REQUEST_NOTIFICATION, *PXUSB_REQUEST_NOTIFICATION; + +// +// Initializes a XUSB_REQUEST_NOTIFICATION structure. +// +VOID FORCEINLINE XUSB_REQUEST_NOTIFICATION_INIT( + _Out_ PXUSB_REQUEST_NOTIFICATION Request, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(Request, sizeof(XUSB_REQUEST_NOTIFICATION)); + + Request->Size = sizeof(XUSB_REQUEST_NOTIFICATION); + Request->SerialNo = SerialNo; +} + +// +// Data structure used in IOCTL_XUSB_SUBMIT_REPORT requests. +// +typedef struct _XUSB_SUBMIT_REPORT +{ + // + // sizeof(struct _XUSB_SUBMIT_REPORT) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // Report to submit to the target device. + // + XUSB_REPORT Report; + +} XUSB_SUBMIT_REPORT, *PXUSB_SUBMIT_REPORT; + +// +// Initializes an XUSB report. +// +VOID FORCEINLINE XUSB_SUBMIT_REPORT_INIT( + _Out_ PXUSB_SUBMIT_REPORT Report, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(Report, sizeof(XUSB_SUBMIT_REPORT)); + + Report->Size = sizeof(XUSB_SUBMIT_REPORT); + Report->SerialNo = SerialNo; +} + +typedef struct _XUSB_GET_USER_INDEX +{ + // + // sizeof(struct _XUSB_GET_USER_INDEX) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // User index of target device. + // + OUT ULONG UserIndex; + +} XUSB_GET_USER_INDEX, *PXUSB_GET_USER_INDEX; + +// +// Initializes XUSB_GET_USER_INDEX structure. +// +VOID FORCEINLINE XUSB_GET_USER_INDEX_INIT( + _Out_ PXUSB_GET_USER_INDEX GetRequest, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(GetRequest, sizeof(XUSB_GET_USER_INDEX)); + + GetRequest->Size = sizeof(XUSB_GET_USER_INDEX); + GetRequest->SerialNo = SerialNo; +} + +#pragma endregion + +#pragma region DualShock 4 section + +typedef struct _DS4_OUTPUT_REPORT +{ + // + // Vibration intensity value of the small motor (0-255). + // + UCHAR SmallMotor; + + // + // Vibration intensity value of the large motor (0-255). + // + UCHAR LargeMotor; + + // + // Color values of the Lightbar. + // + DS4_LIGHTBAR_COLOR LightbarColor; + +} DS4_OUTPUT_REPORT, *PDS4_OUTPUT_REPORT; + +// +// Data structure used in IOCTL_DS4_REQUEST_NOTIFICATION requests. +// +typedef struct _DS4_REQUEST_NOTIFICATION +{ + // + // sizeof(struct _XUSB_REQUEST_NOTIFICATION) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // The HID output report + // + DS4_OUTPUT_REPORT Report; + +} DS4_REQUEST_NOTIFICATION, *PDS4_REQUEST_NOTIFICATION; + +// +// Initializes a DS4_REQUEST_NOTIFICATION structure. +// +VOID FORCEINLINE DS4_REQUEST_NOTIFICATION_INIT( + _Out_ PDS4_REQUEST_NOTIFICATION Request, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(Request, sizeof(DS4_REQUEST_NOTIFICATION)); + + Request->Size = sizeof(DS4_REQUEST_NOTIFICATION); + Request->SerialNo = SerialNo; +} + +// +// DualShock 4 request data +// +typedef struct _DS4_SUBMIT_REPORT +{ + // + // sizeof(struct _DS4_SUBMIT_REPORT) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // HID Input report + // + DS4_REPORT Report; + +} DS4_SUBMIT_REPORT, *PDS4_SUBMIT_REPORT; + +// +// Initializes a DualShock 4 report. +// +VOID FORCEINLINE DS4_SUBMIT_REPORT_INIT( + _Out_ PDS4_SUBMIT_REPORT Report, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(Report, sizeof(DS4_SUBMIT_REPORT)); + + Report->Size = sizeof(DS4_SUBMIT_REPORT); + Report->SerialNo = SerialNo; + + DS4_REPORT_INIT(&Report->Report); +} + +#pragma endregion + +#pragma region XGIP (aka Xbox One device) section - EXPERIMENTAL + +typedef struct _XGIP_REPORT +{ + UCHAR Buttons1; + UCHAR Buttons2; + SHORT LeftTrigger; + SHORT RightTrigger; + SHORT ThumbLX; + SHORT ThumbLY; + SHORT ThumbRX; + SHORT ThumbRY; + +} XGIP_REPORT, *PXGIP_REPORT; + +// +// Xbox One request data +// +typedef struct _XGIP_SUBMIT_REPORT +{ + // + // sizeof(struct _XGIP_SUBMIT_REPORT) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // HID Input report + // + XGIP_REPORT Report; + +} XGIP_SUBMIT_REPORT, *PXGIP_SUBMIT_REPORT; + +// +// Initializes an Xbox One report. +// +VOID FORCEINLINE XGIP_SUBMIT_REPORT_INIT( + _Out_ PXGIP_SUBMIT_REPORT Report, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(Report, sizeof(XGIP_SUBMIT_REPORT)); + + Report->Size = sizeof(XGIP_SUBMIT_REPORT); + Report->SerialNo = SerialNo; +} + +// +// Xbox One interrupt data +// +typedef struct _XGIP_SUBMIT_INTERRUPT +{ + // + // sizeof(struct _XGIP_SUBMIT_INTERRUPT) + // + ULONG Size; + + // + // Serial number of target device. + // + ULONG SerialNo; + + // + // Interrupt buffer. + // + UCHAR Interrupt[64]; + + // + // Length of interrupt buffer. + // + ULONG InterruptLength; + +} XGIP_SUBMIT_INTERRUPT, *PXGIP_SUBMIT_INTERRUPT; + +// +// Initializes an Xbox One interrupt. +// +VOID FORCEINLINE XGIP_SUBMIT_INTERRUPT_INIT( + _Out_ PXGIP_SUBMIT_INTERRUPT Report, + _In_ ULONG SerialNo +) +{ + RtlZeroMemory(Report, sizeof(XGIP_SUBMIT_INTERRUPT)); + + Report->Size = sizeof(XGIP_SUBMIT_INTERRUPT); + Report->SerialNo = SerialNo; +} + +#pragma endregion + diff --git a/third_party/ViGEmClient-1.16.18.0/src/README.md b/third_party/ViGEmClient-1.16.18.0/src/README.md new file mode 100644 index 0000000..5f4e113 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/src/README.md @@ -0,0 +1,8 @@ +# ViGEm user-mode client library +This static library provides the gateway to the bus drivers functionalities. It offers: + * Searching and connecting to a library-compatible bus device on the system + * Attaching and removing a (artificially limited) number of virtual devices + * Feeding the emulated devices (aka providing input state changes) + +## Dependencies +In addition to this library you'll need to link against `setupapi.lib`. \ No newline at end of file diff --git a/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.cpp b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.cpp new file mode 100644 index 0000000..24fed98 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.cpp @@ -0,0 +1,794 @@ +/* +MIT License + +Copyright (c) 2017 Benjamin "Nefarius" Höglinger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include "km/ViGEmBusShared.h" +#include "ViGEmClient.h" +#include +#include +#include + +#define VIGEM_TARGETS_MAX USHRT_MAX + +// +// Represents a driver connection object. +// +typedef struct _VIGEM_CLIENT_T +{ + HANDLE hBusDevice; + +} VIGEM_CLIENT; + +// +// Represents the (connection) state of a target device object. +// +typedef enum _VIGEM_TARGET_STATE +{ + VIGEM_TARGET_NEW, + VIGEM_TARGET_INITIALIZED, + VIGEM_TARGET_CONNECTED, + VIGEM_TARGET_DISCONNECTED +} VIGEM_TARGET_STATE, *PVIGEM_TARGET_STATE; + +// +// Represents a virtual gamepad object. +// +typedef struct _VIGEM_TARGET_T +{ + ULONG Size; + ULONG SerialNo; + VIGEM_TARGET_STATE State; + USHORT VendorId; + USHORT ProductId; + VIGEM_TARGET_TYPE Type; + DWORD_PTR Notification; + +} VIGEM_TARGET; + +// +// Initializes a virtual gamepad object. +// +PVIGEM_TARGET FORCEINLINE VIGEM_TARGET_ALLOC_INIT( + _In_ VIGEM_TARGET_TYPE Type +) +{ + auto target = static_cast(malloc(sizeof(VIGEM_TARGET))); + RtlZeroMemory(target, sizeof(VIGEM_TARGET)); + + target->Size = sizeof(VIGEM_TARGET); + target->State = VIGEM_TARGET_INITIALIZED; + target->Type = Type; + + return target; +} + + +PVIGEM_CLIENT vigem_alloc() +{ + const auto driver = static_cast(malloc(sizeof(VIGEM_CLIENT))); + + if (!driver) + return nullptr; + + RtlZeroMemory(driver, sizeof(VIGEM_CLIENT)); + driver->hBusDevice = INVALID_HANDLE_VALUE; + + return driver; +} + +void vigem_free(PVIGEM_CLIENT vigem) +{ + if (vigem) + free(vigem); +} + +VIGEM_ERROR vigem_connect(PVIGEM_CLIENT vigem) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + SP_DEVICE_INTERFACE_DATA deviceInterfaceData = { 0 }; + deviceInterfaceData.cbSize = sizeof(deviceInterfaceData); + DWORD memberIndex = 0; + DWORD requiredSize = 0; + auto error = VIGEM_ERROR_BUS_NOT_FOUND; + + // check for already open handle as re-opening accidentally would destroy all live targets + if (vigem->hBusDevice != INVALID_HANDLE_VALUE) + { + return VIGEM_ERROR_BUS_ALREADY_CONNECTED; + } + + const auto deviceInfoSet = SetupDiGetClassDevs( + &GUID_DEVINTERFACE_BUSENUM_VIGEM, + nullptr, + nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE + ); + + // enumerate device instances + while (SetupDiEnumDeviceInterfaces( + deviceInfoSet, + nullptr, + &GUID_DEVINTERFACE_BUSENUM_VIGEM, + memberIndex++, + &deviceInterfaceData + )) + { + // get required target buffer size + SetupDiGetDeviceInterfaceDetail(deviceInfoSet, &deviceInterfaceData, nullptr, 0, &requiredSize, nullptr); + + // allocate target buffer + const auto detailDataBuffer = static_cast(malloc(requiredSize)); + detailDataBuffer->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); + + // get detail buffer + if (!SetupDiGetDeviceInterfaceDetail( + deviceInfoSet, + &deviceInterfaceData, + detailDataBuffer, + requiredSize, + &requiredSize, + nullptr + )) + { + SetupDiDestroyDeviceInfoList(deviceInfoSet); + free(detailDataBuffer); + error = VIGEM_ERROR_BUS_NOT_FOUND; + continue; + } + + // bus found, open it + vigem->hBusDevice = CreateFile( + detailDataBuffer->DevicePath, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_OVERLAPPED, + nullptr + ); + + // check bus open result + if (vigem->hBusDevice == INVALID_HANDLE_VALUE) + { + error = VIGEM_ERROR_BUS_ACCESS_FAILED; + free(detailDataBuffer); + continue; + } + + DWORD transfered = 0; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + VIGEM_CHECK_VERSION version; + VIGEM_CHECK_VERSION_INIT(&version, VIGEM_COMMON_VERSION); + + // send compiled library version to driver to check compatibility + DeviceIoControl( + vigem->hBusDevice, + IOCTL_VIGEM_CHECK_VERSION, + &version, + version.Size, + nullptr, + 0, + &transfered, + &lOverlapped + ); + + // wait for result + if (GetOverlappedResult(vigem->hBusDevice, &lOverlapped, &transfered, TRUE) != 0) + { + error = VIGEM_ERROR_NONE; + free(detailDataBuffer); + CloseHandle(lOverlapped.hEvent); + break; + } + + error = VIGEM_ERROR_BUS_VERSION_MISMATCH; + + CloseHandle(lOverlapped.hEvent); + free(detailDataBuffer); + } + + SetupDiDestroyDeviceInfoList(deviceInfoSet); + + return error; +} + +void vigem_disconnect(PVIGEM_CLIENT vigem) +{ + if (!vigem) + return; + + if (vigem->hBusDevice != INVALID_HANDLE_VALUE) + { + CloseHandle(vigem->hBusDevice); + + RtlZeroMemory(vigem, sizeof(VIGEM_CLIENT)); + vigem->hBusDevice = INVALID_HANDLE_VALUE; + } +} + +PVIGEM_TARGET vigem_target_x360_alloc(void) +{ + const auto target = VIGEM_TARGET_ALLOC_INIT(Xbox360Wired); + + if (!target) + return nullptr; + + target->VendorId = 0x045E; + target->ProductId = 0x028E; + + return target; +} + +PVIGEM_TARGET vigem_target_ds4_alloc(void) +{ + const auto target = VIGEM_TARGET_ALLOC_INIT(DualShock4Wired); + + if (!target) + return nullptr; + + target->VendorId = 0x054C; + target->ProductId = 0x05C4; + + return target; +} + +void vigem_target_free(PVIGEM_TARGET target) +{ + if (target) + free(target); +} + +VIGEM_ERROR vigem_target_add(PVIGEM_CLIENT vigem, PVIGEM_TARGET target) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->State == VIGEM_TARGET_NEW) + return VIGEM_ERROR_TARGET_UNINITIALIZED; + + if (target->State == VIGEM_TARGET_CONNECTED) + return VIGEM_ERROR_ALREADY_CONNECTED; + + DWORD transfered = 0; + VIGEM_PLUGIN_TARGET plugin; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + for (target->SerialNo = 1; target->SerialNo <= VIGEM_TARGETS_MAX; target->SerialNo++) + { + VIGEM_PLUGIN_TARGET_INIT(&plugin, target->SerialNo, target->Type); + + plugin.VendorId = target->VendorId; + plugin.ProductId = target->ProductId; + + DeviceIoControl( + vigem->hBusDevice, + IOCTL_VIGEM_PLUGIN_TARGET, + &plugin, + plugin.Size, + nullptr, + 0, + &transfered, + &lOverlapped + ); + + if (GetOverlappedResult(vigem->hBusDevice, &lOverlapped, &transfered, TRUE) != 0) + { + target->State = VIGEM_TARGET_CONNECTED; + CloseHandle(lOverlapped.hEvent); + + return VIGEM_ERROR_NONE; + } + } + + CloseHandle(lOverlapped.hEvent); + + return VIGEM_ERROR_NO_FREE_SLOT; +} + +VIGEM_ERROR vigem_target_add_async(PVIGEM_CLIENT vigem, PVIGEM_TARGET target, PFN_VIGEM_TARGET_ADD_RESULT result) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->State == VIGEM_TARGET_NEW) + return VIGEM_ERROR_TARGET_UNINITIALIZED; + + if (target->State == VIGEM_TARGET_CONNECTED) + return VIGEM_ERROR_ALREADY_CONNECTED; + + std::thread _async{ []( + PVIGEM_TARGET _Target, + PVIGEM_CLIENT _Client, + PFN_VIGEM_TARGET_ADD_RESULT _Result) + { + DWORD transfered = 0; + VIGEM_PLUGIN_TARGET plugin; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + for (_Target->SerialNo = 1; _Target->SerialNo <= VIGEM_TARGETS_MAX; _Target->SerialNo++) + { + VIGEM_PLUGIN_TARGET_INIT(&plugin, _Target->SerialNo, _Target->Type); + + plugin.VendorId = _Target->VendorId; + plugin.ProductId = _Target->ProductId; + + DeviceIoControl(_Client->hBusDevice, IOCTL_VIGEM_PLUGIN_TARGET, &plugin, plugin.Size, nullptr, 0, &transfered, &lOverlapped); + + if (GetOverlappedResult(_Client->hBusDevice, &lOverlapped, &transfered, TRUE) != 0) + { + _Target->State = VIGEM_TARGET_CONNECTED; + CloseHandle(lOverlapped.hEvent); + + if (_Result) + _Result(_Client, _Target, VIGEM_ERROR_NONE); + + return; + } + } + + CloseHandle(lOverlapped.hEvent); + + if (_Result) + _Result(_Client, _Target, VIGEM_ERROR_NO_FREE_SLOT); + + }, target, vigem, result }; + + _async.detach(); + + return VIGEM_ERROR_NONE; +} + +VIGEM_ERROR vigem_target_remove(PVIGEM_CLIENT vigem, PVIGEM_TARGET target) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->State == VIGEM_TARGET_NEW) + return VIGEM_ERROR_TARGET_UNINITIALIZED; + + if (target->State != VIGEM_TARGET_CONNECTED) + return VIGEM_ERROR_TARGET_NOT_PLUGGED_IN; + + DWORD transfered = 0; + VIGEM_UNPLUG_TARGET unplug; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + VIGEM_UNPLUG_TARGET_INIT(&unplug, target->SerialNo); + + DeviceIoControl( + vigem->hBusDevice, + IOCTL_VIGEM_UNPLUG_TARGET, + &unplug, + unplug.Size, + nullptr, + 0, + &transfered, + &lOverlapped + ); + + if (GetOverlappedResult(vigem->hBusDevice, &lOverlapped, &transfered, TRUE) != 0) + { + target->State = VIGEM_TARGET_DISCONNECTED; + CloseHandle(lOverlapped.hEvent); + + return VIGEM_ERROR_NONE; + } + + CloseHandle(lOverlapped.hEvent); + + return VIGEM_ERROR_REMOVAL_FAILED; +} + +VIGEM_ERROR vigem_target_x360_register_notification( + PVIGEM_CLIENT vigem, + PVIGEM_TARGET target, + PFN_VIGEM_X360_NOTIFICATION notification +) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->SerialNo == 0 || notification == nullptr) + return VIGEM_ERROR_INVALID_TARGET; + + if (target->Notification == reinterpret_cast(notification)) + return VIGEM_ERROR_CALLBACK_ALREADY_REGISTERED; + + target->Notification = reinterpret_cast(notification); + + std::thread _async{ []( + PVIGEM_TARGET _Target, + PVIGEM_CLIENT _Client) + { + DWORD error = ERROR_SUCCESS; + DWORD transfered = 0; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + XUSB_REQUEST_NOTIFICATION notify; + XUSB_REQUEST_NOTIFICATION_INIT(¬ify, _Target->SerialNo); + + do + { + DeviceIoControl(_Client->hBusDevice, + IOCTL_XUSB_REQUEST_NOTIFICATION, + ¬ify, + notify.Size, + ¬ify, + notify.Size, + &transfered, + &lOverlapped); + + if (GetOverlappedResult(_Client->hBusDevice, &lOverlapped, &transfered, TRUE) != 0) + { + if (_Target->Notification == NULL) + { + CloseHandle(lOverlapped.hEvent); + return; + } + + reinterpret_cast(_Target->Notification)( + _Client, + _Target, + notify.LargeMotor, + notify.SmallMotor, + notify.LedNumber); + } + else + { + error = GetLastError(); + } + } while (error != ERROR_OPERATION_ABORTED && error != ERROR_ACCESS_DENIED); + + CloseHandle(lOverlapped.hEvent); + + }, target, vigem }; + + _async.detach(); + + return VIGEM_ERROR_NONE; +} + +VIGEM_ERROR vigem_target_ds4_register_notification( + PVIGEM_CLIENT vigem, + PVIGEM_TARGET target, + PFN_VIGEM_DS4_NOTIFICATION notification +) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->SerialNo == 0 || notification == nullptr) + return VIGEM_ERROR_INVALID_TARGET; + + if (target->Notification == reinterpret_cast(notification)) + return VIGEM_ERROR_CALLBACK_ALREADY_REGISTERED; + + target->Notification = reinterpret_cast(notification); + + std::thread _async{ []( + PVIGEM_TARGET _Target, + PVIGEM_CLIENT _Client) + { + DWORD error = ERROR_SUCCESS; + DWORD transfered = 0; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + DS4_REQUEST_NOTIFICATION notify; + DS4_REQUEST_NOTIFICATION_INIT(¬ify, _Target->SerialNo); + + do + { + DeviceIoControl(_Client->hBusDevice, + IOCTL_DS4_REQUEST_NOTIFICATION, + ¬ify, + notify.Size, + ¬ify, + notify.Size, + &transfered, + &lOverlapped); + + if (GetOverlappedResult(_Client->hBusDevice, &lOverlapped, &transfered, TRUE) != 0) + { + if (_Target->Notification == NULL) + { + CloseHandle(lOverlapped.hEvent); + return; + } + + reinterpret_cast(_Target->Notification)( + _Client, + _Target, + notify.Report.LargeMotor, + notify.Report.SmallMotor, + notify.Report.LightbarColor); + } + else + { + error = GetLastError(); + } + } while (error != ERROR_OPERATION_ABORTED && error != ERROR_ACCESS_DENIED); + + CloseHandle(lOverlapped.hEvent); + + }, target, vigem }; + + _async.detach(); + + return VIGEM_ERROR_NONE; +} + +void vigem_target_x360_unregister_notification(PVIGEM_TARGET target) +{ + target->Notification = NULL; +} + +void vigem_target_ds4_unregister_notification(PVIGEM_TARGET target) +{ + target->Notification = NULL; +} + +void vigem_target_set_vid(PVIGEM_TARGET target, USHORT vid) +{ + target->VendorId = vid; +} + +void vigem_target_set_pid(PVIGEM_TARGET target, USHORT pid) +{ + target->ProductId = pid; +} + +USHORT vigem_target_get_vid(PVIGEM_TARGET target) +{ + return target->VendorId; +} + +USHORT vigem_target_get_pid(PVIGEM_TARGET target) +{ + return target->ProductId; +} + +VIGEM_ERROR vigem_target_x360_update( + PVIGEM_CLIENT vigem, + PVIGEM_TARGET target, + XUSB_REPORT report +) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->SerialNo == 0) + return VIGEM_ERROR_INVALID_TARGET; + + DWORD transfered = 0; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + XUSB_SUBMIT_REPORT xsr; + XUSB_SUBMIT_REPORT_INIT(&xsr, target->SerialNo); + + xsr.Report = report; + + DeviceIoControl( + vigem->hBusDevice, + IOCTL_XUSB_SUBMIT_REPORT, + &xsr, + xsr.Size, + nullptr, + 0, + &transfered, + &lOverlapped + ); + + if (GetOverlappedResult(vigem->hBusDevice, &lOverlapped, &transfered, TRUE) == 0) + { + if (GetLastError() == ERROR_ACCESS_DENIED) + { + CloseHandle(lOverlapped.hEvent); + return VIGEM_ERROR_INVALID_TARGET; + } + } + + CloseHandle(lOverlapped.hEvent); + + return VIGEM_ERROR_NONE; +} + +VIGEM_ERROR vigem_target_ds4_update( + PVIGEM_CLIENT vigem, + PVIGEM_TARGET target, + DS4_REPORT report +) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->SerialNo == 0) + return VIGEM_ERROR_INVALID_TARGET; + + DWORD transfered = 0; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + DS4_SUBMIT_REPORT dsr; + DS4_SUBMIT_REPORT_INIT(&dsr, target->SerialNo); + + dsr.Report = report; + + DeviceIoControl( + vigem->hBusDevice, + IOCTL_DS4_SUBMIT_REPORT, + &dsr, + dsr.Size, + nullptr, + 0, + &transfered, + &lOverlapped + ); + + if (GetOverlappedResult(vigem->hBusDevice, &lOverlapped, &transfered, TRUE) == 0) + { + if (GetLastError() == ERROR_ACCESS_DENIED) + { + CloseHandle(lOverlapped.hEvent); + return VIGEM_ERROR_INVALID_TARGET; + } + } + + CloseHandle(lOverlapped.hEvent); + + return VIGEM_ERROR_NONE; +} + +ULONG vigem_target_get_index(PVIGEM_TARGET target) +{ + return target->SerialNo; +} + +VIGEM_TARGET_TYPE vigem_target_get_type(PVIGEM_TARGET target) +{ + return target->Type; +} + +BOOL vigem_target_is_attached(PVIGEM_TARGET target) +{ + return (target->State == VIGEM_TARGET_CONNECTED); +} + +VIGEM_ERROR vigem_target_x360_get_user_index( + PVIGEM_CLIENT vigem, + PVIGEM_TARGET target, + PULONG index +) +{ + if (!vigem) + return VIGEM_ERROR_BUS_INVALID_HANDLE; + + if (!target) + return VIGEM_ERROR_INVALID_TARGET; + + if (vigem->hBusDevice == nullptr) + return VIGEM_ERROR_BUS_NOT_FOUND; + + if (target->SerialNo == 0 || target->Type != Xbox360Wired) + return VIGEM_ERROR_INVALID_TARGET; + + DWORD transfered = 0; + OVERLAPPED lOverlapped = { 0 }; + lOverlapped.hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + XUSB_GET_USER_INDEX gui; + XUSB_GET_USER_INDEX_INIT(&gui, target->SerialNo); + + DeviceIoControl( + vigem->hBusDevice, + IOCTL_XUSB_GET_USER_INDEX, + &gui, + gui.Size, + &gui, + gui.Size, + &transfered, + &lOverlapped + ); + + if (GetOverlappedResult(vigem->hBusDevice, &lOverlapped, &transfered, TRUE) == 0) + { + const auto error = GetLastError(); + + if (error == ERROR_ACCESS_DENIED) + { + CloseHandle(lOverlapped.hEvent); + return VIGEM_ERROR_INVALID_TARGET; + } + + // TODO: handle userindex out-of-range situation here + //if (error == ???) + //{ + // CloseHandle(lOverlapped.hEvent); + // return VIGEM_ERROR_XUSB_USERINDEX_OUT_OF_RANGE; + //} + } + + CloseHandle(lOverlapped.hEvent); + + *index = gui.UserIndex; + + return VIGEM_ERROR_NONE; +} diff --git a/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.rc b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.rc new file mode 100644 index 0000000..352d7ba --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.rc @@ -0,0 +1,100 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// German (Austria) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_DEA) +LANGUAGE LANG_GERMAN, SUBLANG_GERMAN_AUSTRIAN +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,14,3,0 + PRODUCTVERSION 1,14,3,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x7L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000904b0" + BEGIN + VALUE "CompanyName", "Benjamin Höglinger-Stelzer" + VALUE "FileDescription", "Virtual Gamepad Emulation User-Mode Library" + VALUE "FileVersion", "1.14.3.0" + VALUE "InternalName", "Virtual Gamepad Emulation User-Mode Library" + VALUE "LegalCopyright", "Copyright (C) Benjamin Höglinger-Stelzer 2017" + VALUE "OriginalFilename", "ViGEmCli.lib" + VALUE "ProductName", "Virtual Gamepad Emulation User-Mode Library" + VALUE "ProductVersion", "1.14.3.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x9, 1200 + END +END + +#endif // German (Austria) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj new file mode 100644 index 0000000..517d690 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj @@ -0,0 +1,313 @@ + + + + + Debug_DLL + Win32 + + + Debug_DLL + x64 + + + Debug_LIB + Win32 + + + Release_DLL + Win32 + + + Release_DLL + x64 + + + Release_LIB + Win32 + + + Debug_LIB + x64 + + + Release_LIB + x64 + + + + {7DB06674-1F4F-464B-8E1C-172E9587F9DC} + Win32Proj + ViGEmClient + + + + $([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion('Windows', '10.0')) + $(LatestTargetPlatformVersion) + $(WindowsTargetPlatformVersion) + + + + StaticLibrary + true + v141 + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + StaticLibrary + true + v141 + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + StaticLibrary + false + v141 + true + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)lib\debug\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)bin\debug\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)lib\release\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)bin\release\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)lib\debug\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)bin\debug\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)lib\release\$(PlatformShortName)\ + + + $(ProjectDir)../include;$(VC_IncludePath);$(WindowsSDK_IncludePath); + $(SolutionDir)bin\release\$(PlatformShortName)\ + + + + + + Level3 + Disabled + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreadedDebug + + + Windows + + + MachineX86 + + + + + + + Level3 + Disabled + VIGEM_DYNAMIC;VIGEM_EXPORTS;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + + + Windows + setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + + + Level3 + Disabled + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreadedDebug + + + Windows + + + + + + + Level3 + Disabled + VIGEM_DYNAMIC;VIGEM_EXPORTS;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + + + Windows + setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + Level3 + + + MaxSpeed + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreaded + + + Windows + true + true + + + MachineX86 + + + + + Level3 + + + MaxSpeed + true + true + VIGEM_DYNAMIC;VIGEM_EXPORTS;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + Windows + true + true + setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + Level3 + + + MaxSpeed + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + MultiThreaded + + + Windows + true + true + + + + + Level3 + + + MaxSpeed + true + true + VIGEM_DYNAMIC;VIGEM_EXPORTS;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + + + Windows + true + true + setupapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj.filters b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj.filters new file mode 100644 index 0000000..b783fc4 --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/src/ViGEmClient.vcxproj.filters @@ -0,0 +1,44 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/third_party/ViGEmClient-1.16.18.0/src/resource.h b/third_party/ViGEmClient-1.16.18.0/src/resource.h new file mode 100644 index 0000000..ab53b9b --- /dev/null +++ b/third_party/ViGEmClient-1.16.18.0/src/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by ViGEmClient.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif