commit 589ed11e4a35fa5643e8f3949beefb4c21ceb743 Author: Dylan Deverill Date: Sun May 10 19:28:03 2026 -0700 Initial release 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 0000000..b8df728 Binary files /dev/null and b/resources/SteamControllerOFF.ico differ diff --git a/resources/SteamControllerON.ico b/resources/SteamControllerON.ico new file mode 100644 index 0000000..4c52676 Binary files /dev/null and b/resources/SteamControllerON.ico differ diff --git a/resources/app.rc b/resources/app.rc new file mode 100644 index 0000000..b0d9a58 --- /dev/null +++ b/resources/app.rc @@ -0,0 +1,4 @@ +#include "resource.h" + +IDI_ICON_OFF ICON "SteamControllerOFF.ico" +IDI_ICON_ON ICON "SteamControllerON.ico" diff --git a/resources/resource.h b/resources/resource.h new file mode 100644 index 0000000..be4e20b --- /dev/null +++ b/resources/resource.h @@ -0,0 +1,4 @@ +#pragma once + +#define IDI_ICON_OFF 101 +#define IDI_ICON_ON 102 diff --git a/src/app/ControllerManager.cpp b/src/app/ControllerManager.cpp new file mode 100644 index 0000000..e90c7ef --- /dev/null +++ b/src/app/ControllerManager.cpp @@ -0,0 +1,113 @@ +#include "ControllerManager.h" +#include "VirtualController.h" +#include "steam/SteamController.h" +#include + +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