From 8fe1d3ec26571f2114d22706ea088cf30b90838f Mon Sep 17 00:00:00 2001 From: Mifan Bang Date: Sun, 7 Jun 2020 15:39:09 -0700 Subject: [PATCH] Initial commit of source code --- .gitattributes | 22 ++ LICENSE => LICENSE.md | 0 README.md | 58 +++ hagr.rc | 40 ++ hagr.sln | 77 ++++ src/AutoHandle.cpp | 82 ++++ src/AutoHandle.h | 49 +++ src/DebugUtils.cpp | 56 +++ src/DebugUtils.h | 24 ++ src/LightWeightMutex.cpp | 48 +++ src/LightWeightMutex.h | 39 ++ src/Pipes.cpp | 364 ++++++++++++++++++ src/Pipes.h | 189 +++++++++ src/Pro.cpp | 532 ++++++++++++++++++++++++++ src/Pro.h | 70 ++++ src/ProInternals.cpp | 37 ++ src/ProInternals.h | 252 ++++++++++++ src/SteadyTimer.cpp | 33 ++ src/SteadyTimer.h | 34 ++ src/TestMe/main.cpp | 85 ++++ src/hagr.cpp | 212 ++++++++++ src/hagrStubs.cpp | 223 +++++++++++ vcproj/TestMe.vcxproj | 175 +++++++++ vcproj/TestMe.vcxproj.filters | 6 + vcproj/hagr.vcxproj | 222 +++++++++++ vcproj/hagr.vcxproj.filters | 61 +++ vcproj/hagrStub-1_3.vcxproj | 191 +++++++++ vcproj/hagrStub-1_3.vcxproj.filters | 9 + vcproj/hagrStub-9_1_0.vcxproj | 191 +++++++++ vcproj/hagrStub-9_1_0.vcxproj.filters | 9 + vcproj/hagrStub-uap.vcxproj | 191 +++++++++ vcproj/hagrStub-uap.vcxproj.filters | 9 + 32 files changed, 3590 insertions(+) create mode 100644 .gitattributes rename LICENSE => LICENSE.md (100%) create mode 100644 README.md create mode 100644 hagr.rc create mode 100644 hagr.sln create mode 100644 src/AutoHandle.cpp create mode 100644 src/AutoHandle.h create mode 100644 src/DebugUtils.cpp create mode 100644 src/DebugUtils.h create mode 100644 src/LightWeightMutex.cpp create mode 100644 src/LightWeightMutex.h create mode 100644 src/Pipes.cpp create mode 100644 src/Pipes.h create mode 100644 src/Pro.cpp create mode 100644 src/Pro.h create mode 100644 src/ProInternals.cpp create mode 100644 src/ProInternals.h create mode 100644 src/SteadyTimer.cpp create mode 100644 src/SteadyTimer.h create mode 100644 src/TestMe/main.cpp create mode 100644 src/hagr.cpp create mode 100644 src/hagrStubs.cpp create mode 100644 vcproj/TestMe.vcxproj create mode 100644 vcproj/TestMe.vcxproj.filters create mode 100644 vcproj/hagr.vcxproj create mode 100644 vcproj/hagr.vcxproj.filters create mode 100644 vcproj/hagrStub-1_3.vcxproj create mode 100644 vcproj/hagrStub-1_3.vcxproj.filters create mode 100644 vcproj/hagrStub-9_1_0.vcxproj create mode 100644 vcproj/hagrStub-9_1_0.vcxproj.filters create mode 100644 vcproj/hagrStub-uap.vcxproj create mode 100644 vcproj/hagrStub-uap.vcxproj.filters diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..95b4232 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Hagr + +Bridging Nintendo Switch Pro controller and XInput + +## Introduction + +Nintendo Switch Pro controller is a very well designed but also expensive product. Unfortunately it doesn't work on PC out of the box. The reason is that most modern PC games use [XInput](https://en.wikipedia.org/wiki/DirectInput#XInput) to communicate with gamepads but XInput doesn't support Pro controller. Games on Steam may support the device but that's not good enough. + +*Hagr*, meaning skilled or handy in Old Norse, is a tool that communicates with the device and mimics XInput interface so that games can pick up signals from a Pro controller without any modification. + +## How to Use + +Hagr comes in the form of a set of DLLs. There are four DLLs in total and you have to copy them to the folder of your game, *i.e.*, the folder where you can see the EXE file of the game. Remember to back up any `xinput*.dll` file that already exists. Normally a game only needs one or two of the DLLs. Nevertheless, as it's tedious to check which ones your game really needs, just copy all of them and we can call it a day. + +Please note that you DO need to check beforehand whether the game in question is 32-bit or 64-bit. Here's a [quick guide](https://superuser.com/questions/358434/how-to-check-if-a-binary-is-32-or-64-bit-on-windows#889267) of doing it. A 32-bit EXE is unable to load a 64-bit DLL and vise versa. + +## Building the Code + +Just build `hagr.sln` with Visual Studio, preferably 2019. Output binaries will then be located inside `bin/` folder. In the output folder you will also be able to see `TestMe.exe`. It's a simple test program which sends queries to XInput and shows results at a rate of about 60 ticks per second. + +## Limitations + +Hagr is an experimental project. I hope it works for as many use cases as possible but please be expecting situations where it doesn't. There are several limitations which may or may not be resolved in the future. + +- Only **one** controller is supported, and it has to be a Pro controller. +- Only wired connection is supported. +- Thumbstick calibration values are currently hard-coded. +- Vibration via `XInputSetState()` is currently not implemented. +- There's no guarantee that every game using XInput will load the DLLs. Some games have unique ways to start up. + +## An Incomplete List of Working Games + +This is a non-exhaustive list. It only shows those that have been tested by me. + +- Borderlands 3 (64-bit; in `OakGame/Binaries/Win64/` folder) +- Overwatch (64-bit; in `_retail_/` folder) + +## TODO List + +- `XInputSetState()` support. +- Better ways of displaying Hagr status. + +## Acknowledgement + +This project wouldn't be useful in any way without the following pioneer works: + +- [SDL](https://hg.libsdl.org/SDL/file/tip/src/joystick/hidapi/SDL_hidapi_switch.c) +- [dekuNukem/Nintendo_Switch_Reverse_Engineering](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering) + +## Copyright + +Copyright (C) 2020 Mifan Bang . + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/hagr.rc b/hagr.rc new file mode 100644 index 0000000..34eb933 --- /dev/null +++ b/hagr.rc @@ -0,0 +1,40 @@ +#include + +#define RES_APP_VER "0.1.0" +#define RES_APP_VER_INT 0,1,0 + + +LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL + + +///////////////////////////////////////////////////////////////////////////// +// Version + +VS_VERSION_INFO VERSIONINFO + FILEVERSION RES_APP_VER_INT + PRODUCTVERSION RES_APP_VER_INT + FILEFLAGSMASK 0x3fL + FILEFLAGS 0x0L + FILEOS 0x40004L + FILETYPE 0x0L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "041d04b0" + BEGIN + VALUE "CompanyName", "Mifan Bang" + VALUE "FileDescription", "Bridging Nintendo Switch Pro controller and XInput" + VALUE "FileVersion", RES_APP_VER + VALUE "InternalName", "hagr.dll" + VALUE "LegalCopyright", "Copyright (C) 2020 Mifan Bang. https://debug.tw" + VALUE "OriginalFilename", "hagr.dll" + VALUE "ProductName", "Hagr" + VALUE "ProductVersion", RES_APP_VER + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x41d, 0x4b0 + END +END diff --git a/hagr.sln b/hagr.sln new file mode 100644 index 0000000..9eac393 --- /dev/null +++ b/hagr.sln @@ -0,0 +1,77 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29926.136 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hagr", "vcproj\hagr.vcxproj", "{F10755F3-C007-4D2D-9DAD-C46B42377563}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hagrStub-1_3", "vcproj\hagrStub-1_3.vcxproj", "{2F3E60F4-C508-4557-8F89-6E397D8BC68E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hagrStub-9_1_0", "vcproj\hagrStub-9_1_0.vcxproj", "{830344D1-8B8A-4158-82F0-83ACB5E5AC09}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hagrStub-uap", "vcproj\hagrStub-uap.vcxproj", "{86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TestMe", "vcproj\TestMe.vcxproj", "{4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}" + ProjectSection(ProjectDependencies) = postProject + {830344D1-8B8A-4158-82F0-83ACB5E5AC09} = {830344D1-8B8A-4158-82F0-83ACB5E5AC09} + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E} = {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E} + {F10755F3-C007-4D2D-9DAD-C46B42377563} = {F10755F3-C007-4D2D-9DAD-C46B42377563} + {2F3E60F4-C508-4557-8F89-6E397D8BC68E} = {2F3E60F4-C508-4557-8F89-6E397D8BC68E} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Debug|x64.ActiveCfg = Debug|x64 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Debug|x64.Build.0 = Debug|x64 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Debug|x86.ActiveCfg = Debug|Win32 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Debug|x86.Build.0 = Debug|Win32 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Release|x64.ActiveCfg = Release|x64 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Release|x64.Build.0 = Release|x64 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Release|x86.ActiveCfg = Release|Win32 + {F10755F3-C007-4D2D-9DAD-C46B42377563}.Release|x86.Build.0 = Release|Win32 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Debug|x64.ActiveCfg = Debug|x64 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Debug|x64.Build.0 = Debug|x64 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Debug|x86.ActiveCfg = Debug|Win32 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Debug|x86.Build.0 = Debug|Win32 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Release|x64.ActiveCfg = Release|x64 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Release|x64.Build.0 = Release|x64 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Release|x86.ActiveCfg = Release|Win32 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E}.Release|x86.Build.0 = Release|Win32 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Debug|x64.ActiveCfg = Debug|x64 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Debug|x64.Build.0 = Debug|x64 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Debug|x86.ActiveCfg = Debug|Win32 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Debug|x86.Build.0 = Debug|Win32 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Release|x64.ActiveCfg = Release|x64 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Release|x64.Build.0 = Release|x64 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Release|x86.ActiveCfg = Release|Win32 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09}.Release|x86.Build.0 = Release|Win32 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Debug|x64.ActiveCfg = Debug|x64 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Debug|x64.Build.0 = Debug|x64 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Debug|x86.ActiveCfg = Debug|Win32 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Debug|x86.Build.0 = Debug|Win32 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Release|x64.ActiveCfg = Release|x64 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Release|x64.Build.0 = Release|x64 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Release|x86.ActiveCfg = Release|Win32 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E}.Release|x86.Build.0 = Release|Win32 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Debug|x64.ActiveCfg = Debug|x64 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Debug|x64.Build.0 = Debug|x64 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Debug|x86.ActiveCfg = Debug|Win32 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Debug|x86.Build.0 = Debug|Win32 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Release|x64.ActiveCfg = Release|x64 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Release|x64.Build.0 = Release|x64 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Release|x86.ActiveCfg = Release|Win32 + {4FFE9904-19CF-4AB1-A703-4B5A5B13EAB2}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {755C8C48-4D7F-4EC8-A2ED-D93BB41DBCF8} + EndGlobalSection +EndGlobal diff --git a/src/AutoHandle.cpp b/src/AutoHandle.cpp new file mode 100644 index 0000000..67787f7 --- /dev/null +++ b/src/AutoHandle.cpp @@ -0,0 +1,82 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "AutoHandle.h" + +#include + + + +bool IsHandleValid(Handle handle) +{ + return handle != INVALID_HANDLE_VALUE && handle != nullptr; +} + + + +AutoHandle::AutoHandle() noexcept + : m_handle(nullptr) +{ +} + +AutoHandle::AutoHandle(Handle handle) noexcept + : m_handle(handle) +{ +} + +AutoHandle::AutoHandle(AutoHandle&& handle) noexcept + : m_handle(handle.m_handle) +{ + handle.m_handle = nullptr; +} + +AutoHandle::~AutoHandle() noexcept +{ + Close(); +} + +AutoHandle& AutoHandle::operator = (Handle other) noexcept +{ + Close(); + m_handle = other; + return *this; +} + +AutoHandle& AutoHandle::operator = (AutoHandle&& other) noexcept +{ + *this = other.m_handle; // call "operator = (Handle)" version + other.m_handle = nullptr; + return *this; +} + +AutoHandle::operator Handle () const noexcept +{ + return m_handle; +} + +AutoHandle::operator bool () const noexcept +{ + return IsHandleValid(m_handle); +} + +void AutoHandle::Close() noexcept +{ + if (static_cast(*this)) + CloseHandle(m_handle); + m_handle = nullptr; +} diff --git a/src/AutoHandle.h b/src/AutoHandle.h new file mode 100644 index 0000000..8e6d7e7 --- /dev/null +++ b/src/AutoHandle.h @@ -0,0 +1,49 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + + +using Handle = void*; + + +bool IsHandleValid(Handle handle); + + +class AutoHandle +{ +public: + AutoHandle() noexcept; + AutoHandle(Handle handle) noexcept; + AutoHandle(AutoHandle&&) noexcept; + ~AutoHandle() noexcept; + + AutoHandle& operator = (Handle other) noexcept; + AutoHandle& operator = (AutoHandle&& other) noexcept; + + operator bool () const noexcept; + operator Handle () const noexcept; + + void Close() noexcept; + + AutoHandle(const AutoHandle&) = delete; + AutoHandle& operator = (const AutoHandle&) = delete; + +private: + Handle m_handle; +}; diff --git a/src/DebugUtils.cpp b/src/DebugUtils.cpp new file mode 100644 index 0000000..ec518b1 --- /dev/null +++ b/src/DebugUtils.cpp @@ -0,0 +1,56 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DebugUtils.h" + +#include +#include + +#include "Pipes.h" +#include "ProInternals.h" + + + +void DebugOutputString([[maybe_unused]] const wchar_t* str) +{ +#ifdef _DEBUG + OutputDebugStringW(str); +#endif // _DEBUG +} + + +void DebugOutputPacket([[maybe_unused]] const Buffer& buffer) +{ +#ifdef _DEBUG + unsigned int packetIdx = 0; + const auto& funcDumpPacket = [&packetIdx](const Packet& packet) { + std::ostringstream oss; + + oss << packetIdx << ": "; + for (unsigned int i = 0; i < sizeof(packet); ++i) + oss << std::setfill('0') << std::setw(2) << std::uppercase << std::hex << static_cast(reinterpret_cast(&packet)[i]) << " "; + oss << std::endl; + OutputDebugStringA(oss.str().c_str()); + + ++packetIdx; + return true; + }; + + IterateBuffer(buffer, funcDumpPacket); +#endif // _DEBUG +} diff --git a/src/DebugUtils.h b/src/DebugUtils.h new file mode 100644 index 0000000..26cfdfc --- /dev/null +++ b/src/DebugUtils.h @@ -0,0 +1,24 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + + +void DebugOutputString(const wchar_t* str); +void DebugOutputPacket(const struct Buffer& buffer); + diff --git a/src/LightWeightMutex.cpp b/src/LightWeightMutex.cpp new file mode 100644 index 0000000..64b35c0 --- /dev/null +++ b/src/LightWeightMutex.cpp @@ -0,0 +1,48 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LightWeightMutex.h" + +#include + + +LWMutex::LWMutex() + : m_cs(new CRITICAL_SECTION) +{ + InitializeCriticalSection(m_cs.get()); +} + +LWMutex::~LWMutex() +{ + DeleteCriticalSection(m_cs.get()); +} + +void LWMutex::lock() +{ + EnterCriticalSection(m_cs.get()); +} + +bool LWMutex::try_lock() +{ + return TryEnterCriticalSection(m_cs.get()) != FALSE; +} + +void LWMutex::unlock() +{ + LeaveCriticalSection(m_cs.get()); +} diff --git a/src/LightWeightMutex.h b/src/LightWeightMutex.h new file mode 100644 index 0000000..45be3d1 --- /dev/null +++ b/src/LightWeightMutex.h @@ -0,0 +1,39 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + + +struct _RTL_CRITICAL_SECTION; + + +// can be used with std::scoped_lock +class LWMutex +{ +public: + LWMutex(); + ~LWMutex(); + void lock(); + bool try_lock(); + void unlock(); + +private: + std::unique_ptr<_RTL_CRITICAL_SECTION> m_cs; +}; diff --git a/src/Pipes.cpp b/src/Pipes.cpp new file mode 100644 index 0000000..ea95b5c --- /dev/null +++ b/src/Pipes.cpp @@ -0,0 +1,364 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define NOMINMAX + +#include "Pipes.h" +#include "SteadyTimer.h" + +#include +#include + + +class Pipe::Helper +{ +public: + template + static OpResult ExecuteOp(Pipe& pipe, const Func& func) + { + if (!pipe.IsValid()) + return { OpResultCode::InvalidFile, NO_ERROR }; + else if (pipe.IsOpExecuting()) + return { OpResultCode::StillExecuting, NO_ERROR }; + + func(); + + const DWORD lastError = GetLastError(); + if (lastError == ERROR_IO_PENDING || lastError == NO_ERROR) // although NO_ERROR isn't expected but we still treat it as a success + return { OpResultCode::Success, NO_ERROR }; + else + { + pipe.m_overlapped->Internal = 0; // m_overlapped might be set to a bad state + return { OpResultCode::InvalidFile, lastError }; + } + } +}; + + + +Pipe::Pipe(HANDLE file, unsigned int bufferSize) + : m_file(file) + , m_overlapped() + , m_buffer(new Buffer(bufferSize)) +{ + assert(m_buffer != nullptr); + + if (IsHandleValid(m_file)) + { + m_overlapped.reset(new OVERLAPPED); + + constexpr BOOL k_autoReset = FALSE; // we want auto-reset + constexpr BOOL k_initallyCleared = FALSE; + constexpr wchar_t* k_noEventName = nullptr; + HANDLE event = CreateEventW(nullptr, k_autoReset, k_initallyCleared, k_noEventName); + assert(event != NULL); + + ZeroMemory(m_overlapped.get(), sizeof(*m_overlapped)); + m_overlapped->hEvent = event; + } +} + +Pipe::~Pipe() +{ + Close(); +} + +Pipe::SyncResult Pipe::Sync(std::chrono::milliseconds timeout) +{ + if (!IsValid()) + return SyncResult::InvalidFile; + else if (IsOpExecuting()) + { + const auto waitResult = WaitForSingleObject( + m_overlapped->hEvent, + timeout == k_syncInfinite ? INFINITE : static_cast(timeout.count()) // translate infinite case + ); + + if (waitResult == WAIT_OBJECT_0) + return SyncResult::Success; + else if (waitResult == WAIT_TIMEOUT) + return SyncResult::StillExecuting; + else + return SyncResult::InvalidFile; + } + else + return SyncResult::Success; +} + +bool Pipe::IsOpExecuting() const +{ + return m_overlapped && !HasOverlappedIoCompleted(m_overlapped.get()); +} + +unsigned int Pipe::GetBufferSize() const +{ + return m_buffer->size; +} + +bool Pipe::IsValid() const +{ + const auto isOverlappedValid = static_cast(m_overlapped); + const auto isBufferValid = static_cast(m_buffer); + assert(isOverlappedValid == isBufferValid); + return isOverlappedValid; +} + +Pipe& Pipe::operator =(Pipe&& other) +{ + Close(); + + m_file = other.m_file; + other.m_file = INVALID_HANDLE_VALUE; + m_overlapped = std::move(other.m_overlapped); + m_buffer = std::move(other.m_buffer); + + return *this; +} + +void Pipe::CancelOp() +{ + if (IsOpExecuting()) + CancelIoEx(m_file, m_overlapped.get()); +} + +void Pipe::Close() +{ + if (m_overlapped) + { + CancelOp(); + CloseHandle(m_overlapped->hEvent); + } +} + + +ReadPipe::ReadPipe(HANDLE file, unsigned int bufferSize) + : Pipe(file, bufferSize) + , m_isResultConsumed(false) +{ +} + +Pipe::OpResult ReadPipe::Read() +{ + const auto result = Helper::ExecuteOp( + *this, + [this] () { + ReadFile(m_file, m_buffer->data, m_buffer->size, nullptr, m_overlapped.get()); + } + ); + + // because GetResult() returns StillExecuting if an operation is running, there's no worries + // about result from the last successful being pulled. + if (std::get(result) == OpResultCode::Success) + m_isResultConsumed = false; + + return result; +} + +ReadPipe::ReadResult ReadPipe::ReadSync(Buffer& outBuffer, std::chrono::milliseconds timeout) +{ + assert(outBuffer.size <= m_buffer->size); + + const auto result = Read(); + assert(std::get<0>(result) != OpResultCode::StillExecuting); + if (std::get<0>(result) == OpResultCode::Success) + { + const auto syncResult = Sync(timeout); + if (syncResult == SyncResult::Success) + return GetResult(outBuffer); + else if (syncResult == SyncResult::StillExecuting) + return { OpResultCode::StillExecuting, NO_ERROR, 0 }; + else + return { OpResultCode::InvalidFile, GetLastError(), 0 }; + } + + return { std::get<0>(result), std::get<1>(result), 0 }; +} + +ReadPipe::ReadResult ReadPipe::GetResult(Buffer& outBuffer) +{ + assert(outBuffer.size <= m_buffer->size); + + if (!IsValid()) + return { OpResultCode::InvalidFile, NO_ERROR, 0 }; + else if (IsOpExecuting()) + return { OpResultCode::StillExecuting, NO_ERROR, 0 }; + + DWORD bytesRead; + if (m_isResultConsumed) + { + return { OpResultCode::Success, NO_ERROR, 0 }; + } + else if (GetOverlappedResult(m_file, m_overlapped.get(), &bytesRead, FALSE) != 0) + { + m_isResultConsumed = true; + memcpy( + outBuffer.data, + m_buffer->data, + std::min(static_cast(bytesRead), outBuffer.size) + ); + return { OpResultCode::Success, NO_ERROR, bytesRead }; // return the valid length from last device read + } + else + return { OpResultCode::InvalidFile, GetLastError(), 0 }; +} + + +WritePipe::WritePipe(HANDLE file, unsigned int bufferSize) + : Pipe(file, bufferSize) +{ +} + +Pipe::OpResult WritePipe::Write(const Buffer& buffer) +{ + assert(buffer.size <= m_buffer->size); + + if (IsOpExecuting()) + return { OpResultCode::StillExecuting, NO_ERROR }; + + ZeroMemory(m_buffer->data, m_buffer->size); + memcpy(m_buffer->data, buffer.data, buffer.size); + + return Helper::ExecuteOp( + *this, + [this] () { + WriteFile(m_file, m_buffer->data, m_buffer->size, nullptr, m_overlapped.get()); + } + ); +} + +Pipe::OpResult WritePipe::WriteSync(const Buffer& buffer, std::chrono::milliseconds timeout) +{ + const auto result = Write(buffer); + if (std::get(result) == OpResultCode::Success) + return { Sync(timeout), GetLastError() }; + + return result; +} + + +DeviceIoPipes::DeviceIoPipes(AutoHandle&& file, const PipeParams& pipeParams) + : m_file(std::move(file)) + , m_pipeRead(m_file, pipeParams.readBufferSize) + , m_pipeWrite(m_file, pipeParams.writeBufferSize) + , m_mutexRead() + , m_mutexWrite() +{ +} + +DeviceIoPipes::~DeviceIoPipes() +{ +} + +Pipe::OpResult DeviceIoPipes::Read() +{ + std::scoped_lock lock(m_mutexRead); + return m_pipeRead.Read(); +} + +ReadPipe::ReadResult DeviceIoPipes::ReadSync(Buffer& outBuffer, std::chrono::milliseconds timeout) +{ + std::scoped_lock lock(m_mutexRead); + return m_pipeRead.ReadSync(outBuffer, timeout); +} + +ReadPipe::ReadResult DeviceIoPipes::PopReadResult(Buffer& outBuffer) +{ + std::scoped_lock lock(m_mutexRead); + return m_file ? + m_pipeRead.GetResult(outBuffer) : + ReadPipe::ReadResult { Pipe::OpResultCode::InvalidFile, NO_ERROR, 0 }; +} + +Pipe::OpResult DeviceIoPipes::Write(const Buffer& buffer) +{ + std::scoped_lock lock(m_mutexWrite); + return m_pipeWrite.Write(buffer); +} + +Pipe::OpResult DeviceIoPipes::WriteSync(const Buffer& buffer, std::chrono::milliseconds timeout) +{ + std::scoped_lock lock(m_mutexWrite); + return m_pipeWrite.WriteSync(buffer, timeout); +} + +Pipe::SyncResult DeviceIoPipes::SyncRead(std::chrono::milliseconds timeout) +{ + std::scoped_lock lock(m_mutexRead); + return m_pipeRead.Sync(timeout); +} + +Pipe::SyncResult DeviceIoPipes::SyncWrite(std::chrono::milliseconds timeout) +{ + std::scoped_lock lock(m_mutexWrite); + return m_pipeWrite.Sync(timeout); +} + +Pipe::SyncResult DeviceIoPipes::SyncAll(std::chrono::milliseconds timeout) +{ + const SteadyTimer timer; + const auto syncReadResult = SyncRead(timeout); + + // only continue if the read operation finished + if (syncReadResult == Pipe::SyncResult::Success) + { + const auto elaspedTime = timer.GetElapsed(); + + // system overhead may make usedTimeout greater than timeout in which case we consider operation timed out + if (timeout > elaspedTime) + return SyncWrite(timeout - elaspedTime); + else + return Pipe::SyncResult::StillExecuting; + } + + return syncReadResult; +} + +DeviceIoPipes& DeviceIoPipes::operator =(DeviceIoPipes&& other) +{ + std::scoped_lock lock(m_mutexRead, m_mutexWrite, other.m_mutexRead, other.m_mutexWrite); + + m_pipeRead = std::move(other.m_pipeRead); + m_pipeWrite = std::move(other.m_pipeWrite); + m_file = std::move(other.m_file); + other.m_file = INVALID_HANDLE_VALUE; + + return *this; +} + +void DeviceIoPipes::Close() +{ + // closing m_file automatically makes future operations of m_pipeRead and m_pipeWrite return InvalidFile + std::scoped_lock lock(m_mutexRead, m_mutexWrite); + m_file.Close(); +} + +unsigned int DeviceIoPipes::GetReadBufferSize() const +{ + return m_pipeRead.GetBufferSize(); +} + +unsigned int DeviceIoPipes::GetWriteBufferSize() const +{ + return m_pipeWrite.GetBufferSize(); +} + +bool DeviceIoPipes::IsFileValid() const +{ + return m_file; +} + diff --git a/src/Pipes.h b/src/Pipes.h new file mode 100644 index 0000000..0ce9207 --- /dev/null +++ b/src/Pipes.h @@ -0,0 +1,189 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "AutoHandle.h" +#include "LightWeightMutex.h" + + + +struct Buffer +{ + uint32_t size { 0 }; + uint8_t* data { nullptr }; + + Buffer(uint32_t size) + : size(size) + , data(new uint8_t[size]) + { + ZeroMemory(data, size); + } + + ~Buffer() + { + if (data != nullptr) + delete[] data; + } + + template + operator T& () + { + assert(sizeof(T) <= size); + return *reinterpret_cast(data); + } +}; + + +// return true if an invocation of func() returned false +template +bool IterateBuffer(const Buffer& buffer, const F& func) +{ + assert(sizeof(T) <= buffer.size); + const unsigned int numOfT = buffer.size / sizeof(T); + for (unsigned int i = 0; i < numOfT; ++i) + { + const T* instOfT = reinterpret_cast(buffer.data) + i; + if (!func(*instOfT)) + return true; + } + return false; +} + + + +class Pipe +{ +public: + enum class OpResultCode + { + Success, + StillExecuting, // previous operation still executing + InvalidFile // the file is or has become invalid + }; + + using SyncResult = OpResultCode; + using SystemErrorCode = DWORD; + using OpResult = std::tuple; + + static constexpr std::chrono::milliseconds k_syncInfinite { 0 }; + + + Pipe(HANDLE file, unsigned int bufferSize); + ~Pipe(); + + SyncResult Sync(std::chrono::milliseconds timeout); // return false if file is not valid or timed out + + bool IsOpExecuting() const; + unsigned int GetBufferSize() const; + bool IsValid() const; // must be able to check validity as Pipe supports move operation + + Pipe& operator =(Pipe&& other); + + +protected: + class Helper; + + void CancelOp(); + void Close(); // cancel running overlapped operation and relese some resources. the file would still be open. + + + HANDLE m_file; + std::unique_ptr m_overlapped; + std::unique_ptr m_buffer; +}; + + + +class ReadPipe : public Pipe +{ +public: + using ReadResult = std::tuple; + + ReadPipe(HANDLE file, unsigned int bufferSize); + + OpResult Read(); + ReadResult ReadSync(Buffer& outBuffer, std::chrono::milliseconds timeout); + + // for a successful read, only the first call to GetResult() returns a meaningful result. + // all subsequent calls without issuing another read will succeed but have zero data copied to outBuffer. + ReadResult GetResult(Buffer& outBuffer); + + +private: + bool m_isResultConsumed; +}; + + + +class WritePipe : public Pipe +{ +public: + WritePipe(HANDLE file, unsigned int bufferSize); + + OpResult Write(const Buffer& buffer); + OpResult WriteSync(const Buffer& buffer, std::chrono::milliseconds timeout); +}; + + + +class DeviceIoPipes +{ +public: + struct PipeParams + { + unsigned int readBufferSize; + unsigned int writeBufferSize; + }; + + DeviceIoPipes(AutoHandle&& file, const PipeParams& pipeParams); + ~DeviceIoPipes(); + + Pipe::OpResult Read(); + ReadPipe::ReadResult ReadSync(Buffer& outBuffer, std::chrono::milliseconds timeout); + ReadPipe::ReadResult PopReadResult(Buffer& outBuffer); + Pipe::OpResult Write(const Buffer& buffer); + Pipe::OpResult WriteSync(const Buffer& buffer, std::chrono::milliseconds timeout); + + Pipe::SyncResult SyncRead(std::chrono::milliseconds timeout); + Pipe::SyncResult SyncWrite(std::chrono::milliseconds timeout); + Pipe::SyncResult SyncAll(std::chrono::milliseconds timeout); + + DeviceIoPipes& operator =(DeviceIoPipes&& other); + void Close(); + + unsigned int GetReadBufferSize() const; + unsigned int GetWriteBufferSize() const; + bool IsFileValid() const; + + +private: + AutoHandle m_file; + ReadPipe m_pipeRead; + WritePipe m_pipeWrite; + LWMutex m_mutexRead; + LWMutex m_mutexWrite; +}; diff --git a/src/Pro.cpp b/src/Pro.cpp new file mode 100644 index 0000000..11ec12b --- /dev/null +++ b/src/Pro.cpp @@ -0,0 +1,532 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Pro.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "DebugUtils.h" +#include "Pipes.h" +#include "ProInternals.h" +#include "SteadyTimer.h" + + +#pragma comment(lib, "setupapi.lib") + + + +namespace +{ + + +constexpr DeviceIoPipes::PipeParams k_pipeParams = { 128, 64 }; // read = 128 B; write = 64 B +constexpr unsigned int k_pullInterval = 15; // ms; ~60 ticks per second seem like Pro's spec +constexpr uint64_t k_packetTimeout = 100; // ms; for how long the cached states are considered invalid and controller disconnected +constexpr std::chrono::milliseconds k_cmdReplyTimeout(400); // for how long we wait for device to reply to a certain command + + +template +class Tearoff +{ +public: + __forceinline Tearoff(const F& func, unsigned int count) + : m_func(func) + , m_count(count) + { } + + __forceinline bool IsAvailable() const + { + return m_count > 0; + } + + template + typename std::invoke_result::type operator () (Args&&... args) + { + assert(m_count > 0); + const auto result = m_func(std::forward(args)...); + const auto lastCount = m_count; + --m_count; + return result; + } + + template + std::optional::type> RunSafe(Args&&... args) + { + return IsAvailable() ? std::make_optional(operator()(std::forward(args)...)) : std::nullopt; + } + +private: + const F& m_func; + unsigned int m_count; +}; + + +std::wstring FindDevicePath() +{ + constexpr wchar_t k_devicePathSigPro[] = L"hid#vid_057e&pid_2009"; + + std::wstring foundPath; + + HDEVINFO hDevInfoList = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_HID, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (hDevInfoList == INVALID_HANDLE_VALUE) + return foundPath; + + SP_DEVINFO_DATA devInfoData { sizeof(devInfoData) }; + for (unsigned int i = 0; SetupDiEnumDeviceInfo(hDevInfoList, i, &devInfoData) != FALSE; ++i) + { + SP_DEVICE_INTERFACE_DATA devIntfData { sizeof(devIntfData) }; + if (SetupDiEnumDeviceInterfaces(hDevInfoList, &devInfoData, &GUID_DEVINTERFACE_HID, 0, &devIntfData)) + { + constexpr unsigned int buffSize = 1024; + uint8_t buffer[buffSize]; + auto* devIntfDetail = reinterpret_cast(buffer); + devIntfDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + + if (SetupDiGetDeviceInterfaceDetailW(hDevInfoList, &devIntfData, devIntfDetail, buffSize, nullptr, nullptr) && + wcsstr(devIntfDetail->DevicePath, k_devicePathSigPro) != nullptr) + { + foundPath = devIntfDetail->DevicePath; + break; + } + } + } + + SetupDiDestroyDeviceInfoList(hDevInfoList); + return foundPath; +} + + +HANDLE OpenDevice(const std::wstring& path) +{ + constexpr LPSECURITY_ATTRIBUTES k_noSecurityAttr = nullptr; + constexpr HANDLE k_noTemplateFile = nullptr; + + return CreateFileW( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, // have to let others read and write + k_noSecurityAttr, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + k_noTemplateFile + ); +} + + +const Packet* GetLastPacket(const Buffer& buffer) +{ + const Packet* lastGoodPacket = nullptr; + const auto& funcMatch = [&lastGoodPacket](const Packet& packet) { + if (packet.type == PacketType::Device_FullStates) + lastGoodPacket = &packet; + return true; + }; + + IterateBuffer(buffer, funcMatch); + return lastGoodPacket; +} + + +template +bool ReadUntil(DeviceIoPipes& pipes, const F& func) +{ + bool shouldContinuePulling = true; + Buffer buffer(pipes.GetReadBufferSize()); + const SteadyTimer timer; + while (shouldContinuePulling) + { + const auto elaspedTime = timer.GetElapsed(); + if (elaspedTime > k_cmdReplyTimeout) + return false; + + const auto readResult = std::get(pipes.ReadSync(buffer, k_cmdReplyTimeout - elaspedTime)); + if (readResult != Pipe::OpResultCode::Success) + return false; // either an erorr occurred or operation timed out + + DebugOutputPacket(buffer); + + shouldContinuePulling = !IterateBuffer(buffer, func); + } + return true; +} + + +bool WaitForDeviceCommandReply(DeviceIoPipes& pipes, HostSubPacket::CommandCode cmd) +{ + return ReadUntil(pipes, [cmd](const Packet& packet) { + const bool found = packet.type == PacketType::Device_CommandReply && + packet.GetSubPacket().cmdCode == cmd; + return !found; // return true for mismatches + } ); +} + + +bool WaitForDeviceSubcommandReply(DeviceIoPipes& pipes, HostSubPacket::SubcommandCode subcmd) +{ + return ReadUntil(pipes, [subcmd](const Packet& packet) { + const bool found = packet.type == PacketType::Device_SubcommandReply && + packet.GetSubPacket().subcmdCode == subcmd; + return !found; // return true for mismatches + } ); +} + + +bool SendHostCommand(DeviceIoPipes& devPipes, HostSubPacket::CommandCode cmdCode, bool readReply) +{ + constexpr PacketType k_packetType = PacketType::Host_Command; + + Buffer writeBuffer(k_pipeParams.writeBufferSize); + Packet& packet = writeBuffer; + + ZeroMemory(&packet, sizeof(packet)); + packet.type = k_packetType; + packet.GetSubPacket().cmdCode = cmdCode; + const auto writeResult = devPipes.WriteSync(writeBuffer, Pipe::k_syncInfinite); + if (std::get(writeResult) != Pipe::OpResultCode::Success) + return false; + + if (readReply && !WaitForDeviceCommandReply(devPipes, cmdCode)) + return false; + + return true; +} + + +bool SendHostSubcommand(DeviceIoPipes& devPipes, HostSubPacket::SubcommandCode subcmdCode, uint8_t serialId, uint32_t subcmdData, bool readReply) +{ + constexpr PacketType k_packetType = PacketType::Host_RumbleAndSubcommand; + + Buffer writeBuffer(k_pipeParams.writeBufferSize); + Packet& packet = writeBuffer; + + ZeroMemory(&packet, sizeof(packet)); + packet.type = k_packetType; + auto& rumbleAndSubcmd = packet.GetSubPacket(); + rumbleAndSubcmd.serialId = serialId; + rumbleAndSubcmd.left = HostSubPacket::RumbleParam::Neutral(); + rumbleAndSubcmd.right = HostSubPacket::RumbleParam::Neutral(); + rumbleAndSubcmd.subcmdCode = subcmdCode; + rumbleAndSubcmd.subcmdData = subcmdData; + + const auto writeResult = devPipes.WriteSync(writeBuffer, Pipe::k_syncInfinite); + if (std::get(writeResult) != Pipe::OpResultCode::Success) + return false; + + if (readReply && !WaitForDeviceSubcommandReply(devPipes, subcmdCode)) + return false; + + return true; +} + + +template +bool IsSet(T1 val, T2 index) +{ + return ((static_cast(val) >> static_cast(index)) & 1) == 1; +} + + +class PacketAdaptor +{ +public: + static void Translate(const Packet& packet, __out XINPUT_STATE& outputStates, __out XINPUT_BATTERY_INFORMATION& outputBattery) + { + assert(packet.type == PacketType::Device_FullStates); + const auto& gameStates = packet.GetSubPacket(); + + outputStates.dwPacketNumber = gameStates.timestamp; + + const auto [leftX, leftY] = gameStates.leftStick.Split(); + const auto [rightX, rightY] = gameStates.rightStick.Split(); + outputStates.Gamepad.sThumbLX = RemapAxis(leftX); + outputStates.Gamepad.sThumbLY = RemapAxis(leftY); + outputStates.Gamepad.sThumbRX = RemapAxis(rightX); + outputStates.Gamepad.sThumbRY = RemapAxis(rightY); + + const uint32_t buttons = gameStates.keys; + outputStates.Gamepad.bLeftTrigger = IsSet(buttons, Buttons::ZL) ? 0xFF : 0; // Unlike XBO, Pro's triggers are binary + outputStates.Gamepad.bRightTrigger = IsSet(buttons, Buttons::ZR) ? 0xFF : 0; + outputStates.Gamepad.wButtons = 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Y) ? XINPUT_GAMEPAD_X : 0; // Pro's X is at the physical position of Xbox's Y + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::X) ? XINPUT_GAMEPAD_Y : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::B) ? XINPUT_GAMEPAD_A : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::A) ? XINPUT_GAMEPAD_B : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::R) ? XINPUT_GAMEPAD_RIGHT_SHOULDER : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Minus) ? XINPUT_GAMEPAD_BACK : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Plus) ? XINPUT_GAMEPAD_START : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::TriggerR) ? XINPUT_GAMEPAD_RIGHT_THUMB : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::TriggerL) ? XINPUT_GAMEPAD_LEFT_THUMB : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Down) ? XINPUT_GAMEPAD_DPAD_DOWN : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Up) ? XINPUT_GAMEPAD_DPAD_UP : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Right) ? XINPUT_GAMEPAD_DPAD_RIGHT : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::Left) ? XINPUT_GAMEPAD_DPAD_LEFT : 0; + outputStates.Gamepad.wButtons |= IsSet(buttons, Buttons::L) ? XINPUT_GAMEPAD_LEFT_SHOULDER : 0; + + outputBattery.BatteryType = BATTERY_TYPE_NIMH; // doesn't really matter so we hard-code this + outputBattery.BatteryLevel = DecodeBatteryLevel(gameStates.batteryAndWired); + } + +private: + +#define DEFINE_REMAP_CONFIG(name, M, m, neu) \ + enum class name : int16_t { \ + max = M, \ + min = m, \ + neutral = neu \ + }; + DEFINE_REMAP_CONFIG(RamapConfigLeftX, 0xE20, 0x220, 0x7E0); + DEFINE_REMAP_CONFIG(RamapConfigLeftY, 0xE20, 0x1B0, 0x7A0); + DEFINE_REMAP_CONFIG(RamapConfigRightX, 0xE00, 0x230, 0x800); + DEFINE_REMAP_CONFIG(RamapConfigRightY, 0xE20, 0x150, 0x770); +#undef DEFINE_REMAP_CONFIG + + + template + static int16_t RemapAxis(uint16_t value) + { + constexpr int16_t k_max = static_cast(Config::max); + constexpr int16_t k_min = static_cast(Config::min); + constexpr int16_t k_neutral = static_cast(Config::neutral); + + const float signedVal = static_cast(std::clamp(static_cast(value), k_min, k_max) - k_neutral); + if (signedVal > 0.f) + { + constexpr float rangeOrig = k_max - k_neutral; + constexpr float invRangeOrig = 1.f / rangeOrig; + return static_cast(signedVal * invRangeOrig * 0x7FFF); + } + else if (signedVal < 0.f) + { + constexpr float rangeOrig = k_neutral - k_min; + constexpr float invRangeOrig = 1.f / rangeOrig; + return static_cast(signedVal * invRangeOrig * 0x8000); + } + else + return 0; + } + + static uint8_t DecodeBatteryLevel(uint8_t batteryAndWired) + { + // 0 is EMPTY; remap the rest [1-8] to [1-3], where BATTERY_LEVEL_LOW=1, MEDIUM=2, FULL=3 + const uint8_t battery = batteryAndWired >> 4; + if (battery >= 7) + return BATTERY_LEVEL_FULL; + else if (battery >= 4) + return BATTERY_LEVEL_MEDIUM; + else if (battery >= 1) + return BATTERY_LEVEL_LOW; + else + return BATTERY_LEVEL_EMPTY; + } +}; + + +} // unnamed namespace + + + +// ---------------------------------------------------------------------------- +// ProAgent definitions ------------------------------------------------------- + +ProAgent::CachedStates::CachedStates() + : timestamp(0) + , mutex() + , gamepad() + , battery() +{ + std::unique_lock lock(mutex); + + ZeroMemory(&gamepad, sizeof(gamepad)); + ZeroMemory(&battery, sizeof(battery)); +} + +ProAgent::ProAgent() + : m_devPipes(OpenDevice(FindDevicePath()), k_pipeParams) + , m_cachedStates() + , m_workerThread() + , m_workerStopSignal(false) +{ + InitWorkerThread(); +} + +ProAgent::~ProAgent() +{ + if (m_workerThread) + { + m_workerStopSignal = true; + m_workerThread->join(); + } +} + +// return true if result is cached or being read from device; return false otherwise +bool ProAgent::TryUpdate() +{ + // only try to reattach once in each tick + Tearoff reattachRequest(std::mem_fn(&ProAgent::ReattachToDevice), 1); + + if (!m_devPipes.IsFileValid() && !reattachRequest(this)) + return false; + + Buffer buffer(k_pipeParams.readBufferSize); + const auto popResultCode = std::get(m_devPipes.PopReadResult(buffer)); + if (popResultCode == Pipe::OpResultCode::InvalidFile) + { + m_devPipes.Close(); + reattachRequest.RunSafe(this); + return false; // we don't have results ready in this tick + } + else if (popResultCode == Pipe::OpResultCode::StillExecuting) + { + // if PopReadResult() keeps returning StillExecuting, it could mean another process, e.g. Steam, is + // communicating with the device and somehow forces it into sleep mode. + if (GetTickCount64() - m_cachedStates.timestamp > k_packetTimeout) + { + m_devPipes.Close(); + reattachRequest.RunSafe(this); + return false; + } + } + else if (popResultCode == Pipe::OpResultCode::Success) + { + // issue next read before processing packets to maximize throughput + const auto readResultCode = std::get(m_devPipes.Read()); + + // now process packets + if (const auto* packet = GetLastPacket(buffer)) + { + std::unique_lock lock(m_cachedStates.mutex); + m_cachedStates.timestamp = GetTickCount64(); + PacketAdaptor::Translate(*packet, m_cachedStates.gamepad, m_cachedStates.battery); + } + + // handle failed read operation only after caching states + if (readResultCode == Pipe::OpResultCode::InvalidFile) + { + // still return true as this tick successfully updated cache + m_devPipes.Close(); + reattachRequest.RunSafe(this); + } + } + return true; +} + +bool ProAgent::GetCachedState(__out XINPUT_STATE& result) const +{ + std::shared_lock lock(m_cachedStates.mutex); + if (GetTickCount64() - m_cachedStates.timestamp < k_packetTimeout) + { + result = m_cachedStates.gamepad; + return true; + } + return false; +} + +bool ProAgent::GetBatteryInfo(__out XINPUT_BATTERY_INFORMATION& result) const +{ + std::shared_lock lock(m_cachedStates.mutex); + if (GetTickCount64() - m_cachedStates.timestamp < k_packetTimeout) + { + result = m_cachedStates.battery; + return true; + } + return false; +} + +bool ProAgent::IsDeviceValid() const +{ + return m_devPipes.IsFileValid(); +} + +void ProAgent::InitWorkerThread() +{ + if (m_workerThread) + return; + + auto* thread = new std::thread(std::mem_fn(&ProAgent::WorkerThreadProc), this); + m_workerThread.reset(thread); +} + +bool ProAgent::ReattachToDevice() +{ + if (AutoHandle newDeviceFile = OpenDevice(FindDevicePath())) + { + DeviceIoPipes newDevicePipes(std::move(newDeviceFile), k_pipeParams); + m_devPipes = std::move(newDevicePipes); + InitDevice(); + + return true; + } + return false; +} + +bool ProAgent::InitDevice() +{ + using HostSubPacket::CommandCode; + using HostSubPacket::SubcommandCode; + + // raw data: 0x80 0x02 + DebugOutputString(L"HostCommand=HandShake\n"); + if (!SendHostCommand(m_devPipes, CommandCode::HandShake, true)) + return false; + + // raw data: 0x80 0x03 + DebugOutputString(L"HostCommand=SetHighSpeed\n"); + if (!SendHostCommand(m_devPipes, CommandCode::SetHighSpeed, true)) + return false; + + // raw data: 0x80 0x02 + DebugOutputString(L"HostCommand=HandShake\n"); + if (!SendHostCommand(m_devPipes, CommandCode::HandShake, true)) + return false; + + // raw data: 0x08 0x04 + // device doesn't generate reply to this command code + DebugOutputString(L"HostCommand=ForceUSB\n"); + if (!SendHostCommand(m_devPipes, CommandCode::ForceUSB, false)) + return false; + + // turn on player 0 light + constexpr uint32_t k_playerLEDIndex = 1; + DebugOutputString(L"HostSubcommand=Host_RumbleAndSubcommand\n"); + if (!SendHostSubcommand(m_devPipes, SubcommandCode::SetPlayerLights, 1, k_playerLEDIndex, true)) + return false; + + return true; +} + +void ProAgent::WorkerThreadProc() +{ + if (m_devPipes.IsFileValid()) + InitDevice(); + + while (!m_workerStopSignal) + { + TryUpdate(); + Sleep(k_pullInterval); + } +} + diff --git a/src/Pro.h b/src/Pro.h new file mode 100644 index 0000000..e2e8d21 --- /dev/null +++ b/src/Pro.h @@ -0,0 +1,70 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include +#include + +#include "Pipes.h" + + + +class ProAgent +{ +public: + ProAgent(); + ~ProAgent(); + + bool GetCachedState(__out XINPUT_STATE& result) const; + bool GetBatteryInfo(__out XINPUT_BATTERY_INFORMATION& result) const; + bool IsDeviceValid() const; + + +private: + void InitWorkerThread(); + bool ReattachToDevice(); + bool InitDevice(); // NS Pro controller needs to be initialized via a private protocol + bool TryUpdate(); + void WorkerThreadProc(); + + + struct CachedStates + { + // book-keeping + uint64_t timestamp; + mutable std::shared_mutex mutex; // VS' implementation is simple enough so use STL here + + // actual data + XINPUT_STATE gamepad; + XINPUT_BATTERY_INFORMATION battery; + + CachedStates(); + }; + + + DeviceIoPipes m_devPipes; + CachedStates m_cachedStates; + + std::unique_ptr m_workerThread; + volatile bool m_workerStopSignal; +}; + diff --git a/src/ProInternals.cpp b/src/ProInternals.cpp new file mode 100644 index 0000000..8ae44c4 --- /dev/null +++ b/src/ProInternals.cpp @@ -0,0 +1,37 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ProInternals.h" + + +bool Packet::IsTypeValid() const +{ + switch (type) + { + case PacketType::Host_RumbleAndSubcommand: + case PacketType::Host_Rumble: + case PacketType::Host_Command: + case PacketType::Device_SubcommandReply: + case PacketType::Device_FullStates: + case PacketType::Device_CommandReply: + return true; + default: + return false; + } +} + diff --git a/src/ProInternals.h b/src/ProInternals.h new file mode 100644 index 0000000..16b2912 --- /dev/null +++ b/src/ProInternals.h @@ -0,0 +1,252 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + + + +#pragma pack(push, nspro, 1) + + + +// ---------------------------------------------------------------------------- +// data types ----------------------------------------------------------------- + +struct UInt24 +{ + uint8_t bytes[3]; + + __forceinline operator uint32_t () const + { + return (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]; // little endian + } + + __forceinline std::tuple Split() const + { + return { + static_cast(bytes[0] | ((bytes[1] & 0xF) << 8)), + static_cast((bytes[2] << 4) | (bytes[1] >> 4)) + }; + } +}; + + +enum class Buttons +{ + Y = 0, + X, + B, + A, + // [4, 5] unmapped + R = 6, + ZR, + Minus, + Plus, + TriggerR, + TriggerL, + Home, + Share, + // [14, 15] unmapped + Down = 16, + Up, + Right, + Left, + // [20, 21] unmapped + L = 22, + ZL +}; + + + +// ---------------------------------------------------------------------------- +// subpackets sent from host -------------------------------------------------- + +namespace HostSubPacket +{ + enum class SubcommandCode : uint8_t + { + SetPlayerLights = 0x30, + SetIMUSensitivity = 0x41, + }; + + enum class CommandCode : uint8_t + { + HandShake = 0x02, + SetHighSpeed = 0x03, + ForceUSB = 0x04 + }; + + struct RumbleParam + { + uint8_t highFreq; + uint8_t highFreqAmp; + uint8_t lowFreq; + uint8_t lowFreqAmp; + + constexpr static RumbleParam Neutral() + { + return { 0x00, 0x01, 0x40, 0x40 }; + } + }; + + + // 0x01 + struct RumbleAndSubcommand + { + uint8_t serialId; + RumbleParam left; + RumbleParam right; + SubcommandCode subcmdCode; + uint32_t subcmdData; + }; + + // 0x10 + struct Rumble + { + RumbleParam left; + RumbleParam right; + }; + + // 0x80 + struct Command + { + CommandCode cmdCode; + }; + +} // namespace HostSubPacket + + + +// ---------------------------------------------------------------------------- +// subpackets sent from client ------------------------------------------------ + +namespace DeviceSubPacket +{ + // shared data structure embedded in multiple types of packet + struct CommonStates + { + uint8_t timestamp; + uint8_t batteryAndWired; + UInt24 keys; + UInt24 leftStick; + UInt24 rightStick; + uint8_t vibration; + }; + + // 0x21 + struct SubcommandReply : CommonStates + { + uint8_t subcmdAck; // success if bit index 7 is set(?) + HostSubPacket::SubcommandCode subcmdCode; // same as subcommand code sent in RumbleAndSubcommand packet + uint32_t data; // unknown + }; + + // 0x30 + struct FullStates : CommonStates + { + // no additional fields + }; + + // 0x81 + struct CommandReply + { + HostSubPacket::CommandCode cmdCode; + }; +} // namespace DeviceSubPacket + + + +// ---------------------------------------------------------------------------- +// packet --------------------------------------------------------------------- + +enum class PacketType : uint8_t +{ + Host_RumbleAndSubcommand = 0x01, + Host_Rumble = 0x10, + Host_Command = 0x80, + Device_SubcommandReply = 0x21, // reply to Host_RumbleAndSubcommand + Device_FullStates = 0x30, + Device_CommandReply = 0x81, // reply to Host_Command +}; + +// type traits for all subpackets +template struct ToPacketType { }; + +#define DEFINE_PACKET_TYPE_MAPPING(enumValue, typeStruct) \ + template <> struct ToPacketType \ + { using type = typeStruct; }; + +DEFINE_PACKET_TYPE_MAPPING(PacketType::Host_RumbleAndSubcommand, HostSubPacket::RumbleAndSubcommand); +DEFINE_PACKET_TYPE_MAPPING(PacketType::Host_Rumble, HostSubPacket::Rumble); +DEFINE_PACKET_TYPE_MAPPING(PacketType::Host_Command, HostSubPacket::Command); +DEFINE_PACKET_TYPE_MAPPING(PacketType::Device_SubcommandReply, DeviceSubPacket::SubcommandReply); +DEFINE_PACKET_TYPE_MAPPING(PacketType::Device_FullStates, DeviceSubPacket::FullStates); +DEFINE_PACKET_TYPE_MAPPING(PacketType::Device_CommandReply, DeviceSubPacket::CommandReply); +#undef DEFINE_PACKET_TYPE_MAPPING + + +struct Packet +{ + PacketType type; + union + { + HostSubPacket::RumbleAndSubcommand rumbleAndSubcommand; + HostSubPacket::Rumble rumble; + HostSubPacket::Command command; + + DeviceSubPacket::SubcommandReply subcommandReply; + DeviceSubPacket::FullStates fullStates; + DeviceSubPacket::CommandReply commandReply; + + uint8_t unused[63]; + }; + + bool IsTypeValid() const; + + // subpacket getters - deleted by default + template typename ToPacketType::type& GetSubPacket() = delete; + template const typename ToPacketType::type& GetSubPacket() const = delete; + + // subpacket getter specializations +#define DEFINE_SUBPACKET_GETTER(enumType, memberName) \ + template<> ToPacketType::type& GetSubPacket() { return memberName; } \ + template<> const ToPacketType::type& GetSubPacket() const { return memberName; } + + DEFINE_SUBPACKET_GETTER(PacketType::Host_RumbleAndSubcommand, rumbleAndSubcommand); + DEFINE_SUBPACKET_GETTER(PacketType::Host_Rumble, rumble); + DEFINE_SUBPACKET_GETTER(PacketType::Host_Command, command); + DEFINE_SUBPACKET_GETTER(PacketType::Device_SubcommandReply, subcommandReply); + DEFINE_SUBPACKET_GETTER(PacketType::Device_FullStates, fullStates); + DEFINE_SUBPACKET_GETTER(PacketType::Device_CommandReply, commandReply); +#undef DEFINE_SUBPACKET_GETTER + + constexpr size_t GetSize() + { + return sizeof(Packet); + } +}; + +static_assert(sizeof(Packet) == 64); + + + +#pragma pack(pop, nspro) + diff --git a/src/SteadyTimer.cpp b/src/SteadyTimer.cpp new file mode 100644 index 0000000..56de5cc --- /dev/null +++ b/src/SteadyTimer.cpp @@ -0,0 +1,33 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SteadyTimer.h" + +#include + + + +SteadyTimer::SteadyTimer() + : m_start(GetTickCount64()) +{ +} + +std::chrono::milliseconds SteadyTimer::GetElapsed() const +{ + return std::chrono::milliseconds(GetTickCount64() - m_start); +} diff --git a/src/SteadyTimer.h b/src/SteadyTimer.h new file mode 100644 index 0000000..abb145e --- /dev/null +++ b/src/SteadyTimer.h @@ -0,0 +1,34 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + + +class SteadyTimer +{ +public: + SteadyTimer(); + + std::chrono::milliseconds GetElapsed() const; + +private: + const uint64_t m_start; +}; diff --git a/src/TestMe/main.cpp b/src/TestMe/main.cpp new file mode 100644 index 0000000..183043e --- /dev/null +++ b/src/TestMe/main.cpp @@ -0,0 +1,85 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include +#include + +#pragma comment(lib, "xinput.lib") + + + +void EnableConsoleColoring(HANDLE console) +{ + DWORD consoleMode; + if (GetConsoleMode(console, &consoleMode)) + SetConsoleMode(console, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); +} + + +int main() +{ + constexpr DWORD k_playerID = 0; + constexpr DWORD k_sleepInterval = 16; + constexpr COORD k_cursorOrigin = { 0, 0 }; + + HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE); + EnableConsoleColoring(console); + + XINPUT_STATE state; + XINPUT_BATTERY_INFORMATION batteryInfo; + while (true) + { + SetConsoleCursorPosition(console, k_cursorOrigin); + + ZeroMemory(&state, sizeof(state)); + ZeroMemory(&batteryInfo, sizeof(batteryInfo)); + const DWORD getStateResult = XInputGetState(k_playerID, &state); + XInputGetBatteryInformation(k_playerID, BATTERY_DEVTYPE_GAMEPAD, &batteryInfo); + + printf("%sResult code: %08X\033[0m\n", + getStateResult == 0 ? "\033[0;32m" : "\033[0;31m", // color + getStateResult + ); + printf("System tick: %lld\n" + "Input states:\n" + " Timestamp = %02X\n" + " Buttons = %04X\n" + " Left trigger = %3d\n" + " Right trigger = %3d\n" + " Left thumbstick = (%+6d, %+6d)\n" + " Right thumbstick = (%+6d, %+6d)\n" + "Battery info:\n" + " Type = %02X\n" + " Level = %02X\n", + GetTickCount64(), + state.dwPacketNumber, + state.Gamepad.wButtons, + state.Gamepad.bLeftTrigger, + state.Gamepad.bRightTrigger, + state.Gamepad.sThumbLX, state.Gamepad.sThumbLY, + state.Gamepad.sThumbRX, state.Gamepad.sThumbRY, + batteryInfo.BatteryType, + batteryInfo.BatteryLevel); + + Sleep(k_sleepInterval); + } + + return NO_ERROR; +} diff --git a/src/hagr.cpp b/src/hagr.cpp new file mode 100644 index 0000000..90bb74f --- /dev/null +++ b/src/hagr.cpp @@ -0,0 +1,212 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifdef _DEBUG + #include + #define dbgPrint printf +#else + #define dbgPrint +#endif // _DEBUG + +#include +#include + +#include "Pro.h" + + + +namespace +{ + +ProAgent* g_proAgent = nullptr; + +} // unnames namespace + + +extern "C" +{ + + +DWORD __stdcall _XInputGetState( + DWORD dwUserIndex, + __out XINPUT_STATE* pState) +{ + if (!g_proAgent->IsDeviceValid() || dwUserIndex > 0) + return ERROR_DEVICE_NOT_CONNECTED; + + const bool result = g_proAgent->GetCachedState(*pState); + dbgPrint("XInputGetState %d %04X %08X\n", result, pState->dwPacketNumber, pState->Gamepad.wButtons); + return result ? NO_ERROR : static_cast(-1); +} + + +DWORD __stdcall _XInputSetState( + DWORD dwUserIndex, + [[maybe_unused]] XINPUT_VIBRATION* pVibration) +{ + dbgPrint("XInputSetState %d\n", dwUserIndex); + + if (!g_proAgent->IsDeviceValid() || dwUserIndex > 0) + return ERROR_DEVICE_NOT_CONNECTED; + + return NO_ERROR; +} + + +DWORD __stdcall _XInputGetCapabilities( + DWORD dwUserIndex, + [[maybe_unused]] DWORD dwFlags, + __out XINPUT_CAPABILITIES* pCapabilities) +{ + dbgPrint("XInputGetCapabilities\n"); + + if (!g_proAgent->IsDeviceValid() || dwUserIndex > 0) + return ERROR_DEVICE_NOT_CONNECTED; + + // values read from a real Xbox One controller connected with USB cable + pCapabilities->Type = XINPUT_DEVTYPE_GAMEPAD; + pCapabilities->SubType = XINPUT_DEVSUBTYPE_GAMEPAD; + pCapabilities->Flags = 0; + pCapabilities->Gamepad.wButtons = 0xF3FF; + pCapabilities->Gamepad.bLeftTrigger = 0xFF; + pCapabilities->Gamepad.bRightTrigger = 0xFF; + pCapabilities->Gamepad.sThumbLX = static_cast(0xFFC0); + pCapabilities->Gamepad.sThumbLY = static_cast(0xFFC0); + pCapabilities->Gamepad.sThumbRX = static_cast(0xFFC0); + pCapabilities->Gamepad.sThumbRY = static_cast(0xFFC0); + pCapabilities->Vibration.wLeftMotorSpeed = 0xFF; + pCapabilities->Vibration.wRightMotorSpeed = 0xFF; + + return NO_ERROR; +} + + +void __stdcall _XInputEnable(BOOL enable) +{ + dbgPrint("XInputEnable %d\n", enable); +} + + +DWORD __stdcall _XInputGetAudioDeviceIds( + DWORD dwUserIndex, + [[maybe_unused]] __out_ecount_opt(*pRenderCount) LPWSTR pRenderDeviceId, + [[maybe_unused]] __inout_opt UINT* pRenderCount, + [[maybe_unused]] __out_ecount_opt(*pCaptureCount) LPWSTR pCaptureDeviceId, + [[maybe_unused]] __inout_opt UINT* pCaptureCount) +{ + dbgPrint("XInputGetAudioDeviceIds\n"); + + if (!g_proAgent->IsDeviceValid() || dwUserIndex > 0) + return ERROR_DEVICE_NOT_CONNECTED; + + return ERROR_DEVICE_NOT_CONNECTED; +} + + +DWORD __stdcall _XInputGetBatteryInformation( + DWORD dwUserIndex, + BYTE devType, + __out XINPUT_BATTERY_INFORMATION* pBatteryInformation) +{ + if (!g_proAgent->IsDeviceValid() || dwUserIndex > 0 || devType != BATTERY_DEVTYPE_GAMEPAD) + return ERROR_DEVICE_NOT_CONNECTED; + + const bool result = g_proAgent->GetBatteryInfo(*pBatteryInformation); + dbgPrint("XInputGetBatteryInformation %d %02X %02X\n", result, pBatteryInformation->BatteryType, pBatteryInformation->BatteryLevel); + return result ? NO_ERROR : static_cast(-1); +} + + +DWORD __stdcall _XInputGetKeystroke( + DWORD dwUserIndex, + [[maybe_unused]] __reserved DWORD dwReserved, + [[maybe_unused]] __out XINPUT_KEYSTROKE* pKeystroke) +{ + dbgPrint("XInputGetKeystroke\n"); + + if (!g_proAgent->IsDeviceValid() || dwUserIndex > 0) + return ERROR_DEVICE_NOT_CONNECTED; + + return ERROR_EMPTY; // we basically don't support this function +} + + +DWORD __stdcall _XInputGetDSoundAudioDeviceGuids( + [[maybe_unused]] DWORD dwUserIndex, + [[maybe_unused]] __out GUID* pDSoundRenderGuid, + [[maybe_unused]] __out GUID* pDSoundCaptureGuid) +{ + dbgPrint("XInputGetDSoundAudioDeviceGuids\n"); + + return ERROR_DEVICE_NOT_CONNECTED; +} + + +} // extern "C" + + +__declspec(dllexport) BOOL __stdcall DllMain( + [[maybe_unused]] HINSTANCE hinstDLL, + DWORD fdwReason, + [[maybe_unused]] void* lpReserved) +{ + if (fdwReason == DLL_PROCESS_ATTACH) + { +#if _DEBUG + FILE* fp; + AllocConsole(); + freopen_s(&fp, "CONIN$", "r+t", stdin); + freopen_s(&fp, "CONOUT$", "w+t", stdout); + freopen_s(&fp, "CONOUT$", "w+t", stderr); +#endif + g_proAgent = new ProAgent(); + } + else if (fdwReason == DLL_PROCESS_DETACH) + { + delete g_proAgent; + g_proAgent = nullptr; + } + + return TRUE; +} + + + +// export configuration sets +// x64 uses undecorated names while x86 uses decorated ones +#if defined _WIN64 + #pragma comment(linker, "/export:DllMain,@1") + #pragma comment(linker, "/export:XInputGetState=_XInputGetState,@2") + #pragma comment(linker, "/export:XInputSetState=_XInputSetState,@3") + #pragma comment(linker, "/export:XInputGetCapabilities=_XInputGetCapabilities,@4") + #pragma comment(linker, "/export:XInputEnable=_XInputEnable,@5") + #pragma comment(linker, "/export:XInputGetAudioDeviceIds=_XInputGetAudioDeviceIds,@6") + #pragma comment(linker, "/export:XInputGetBatteryInformation=_XInputGetBatteryInformation,@7") + #pragma comment(linker, "/export:XInputGetKeystroke=_XInputGetKeystroke,@8") + #pragma comment(linker, "/export:XInputGetDSoundAudioDeviceGuids=_XInputGetDSoundAudioDeviceGuids,@9") +#else + #pragma comment(linker, "/export:DllMain=_DllMain@12,@1") + #pragma comment(linker, "/export:XInputGetState=__XInputGetState@8,@2") + #pragma comment(linker, "/export:XInputSetState=__XInputSetState@8,@3") + #pragma comment(linker, "/export:XInputGetCapabilities=__XInputGetCapabilities@12,@4") + #pragma comment(linker, "/export:XInputEnable=__XInputEnable@4,@5") + #pragma comment(linker, "/export:XInputGetAudioDeviceIds=__XInputGetAudioDeviceIds@20,@6") + #pragma comment(linker, "/export:XInputGetBatteryInformation=__XInputGetBatteryInformation@12,@7") + #pragma comment(linker, "/export:XInputGetKeystroke=__XInputGetKeystroke@12,@8") + #pragma comment(linker, "/export:XInputGetDSoundAudioDeviceGuids=__XInputGetDSoundAudioDeviceGuids@12,@9") +#endif diff --git a/src/hagrStubs.cpp b/src/hagrStubs.cpp new file mode 100644 index 0000000..8cef906 --- /dev/null +++ b/src/hagrStubs.cpp @@ -0,0 +1,223 @@ +/* + * hagr - bridging Nintendo Switch Pro controller and XInput + * Copyright (C) 2020 Mifan Bang . + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + + + +// declaration for the function removed or deprecated in newer SDKs +#if _WIN32_WINNT >= _WIN32_WINNT_WIN8 + + extern "C" DWORD __stdcall XInputGetDSoundAudioDeviceGuids + ( + DWORD dwUserIndex, + __out GUID* pDSoundRenderGuid, + __out GUID* pDSoundCaptureGuid + ); + + extern "C" void __stdcall XInputEnable_NoDeprecation + ( + _In_ BOOL enable + ); + +#endif // _WIN32_WINNT >= _WIN32_WINNT_WIN8 + + + +namespace +{ + + decltype(XInputGetState)* g_fpGetState = nullptr; + decltype(XInputSetState)* g_fpSetState = nullptr; + decltype(XInputGetCapabilities)* g_fpGetCapabilities = nullptr; + decltype(XInputEnable_NoDeprecation)* g_fpEnable = nullptr; + decltype(XInputGetAudioDeviceIds)* g_fpGetAudioDeviceIds = nullptr; + decltype(XInputGetBatteryInformation)* g_fpGetBatteryInformation = nullptr; + decltype(XInputGetKeystroke)* g_fpGetKeystroke = nullptr; + decltype(XInputGetDSoundAudioDeviceGuids)* g_fpGetDSoundAudioDeviceGuids = nullptr; + + template + void AssignFuncPtr(T& funcPtr, FARPROC farproc) + { + funcPtr = reinterpret_cast(farproc); + } + +} // unnamed namespace + + + +extern "C" +{ + + +DWORD __stdcall _XInputGetState(DWORD dwUserIndex, __out XINPUT_STATE* pState) +{ + return g_fpGetState ? g_fpGetState(dwUserIndex, pState) : ERROR_DEVICE_NOT_CONNECTED; +} + + +DWORD __stdcall _XInputSetState(DWORD dwUserIndex, XINPUT_VIBRATION* pVibration) +{ + return g_fpSetState ? g_fpSetState(dwUserIndex, pVibration) : ERROR_DEVICE_NOT_CONNECTED; +} + + +DWORD __stdcall _XInputGetCapabilities(DWORD dwUserIndex, DWORD dwFlags, __out XINPUT_CAPABILITIES* pCapabilities) +{ + return g_fpGetCapabilities ? g_fpGetCapabilities(dwUserIndex, dwFlags, pCapabilities) : ERROR_DEVICE_NOT_CONNECTED; +} + + +void __stdcall _XInputEnable(BOOL enable) +{ + if (g_fpEnable) + g_fpEnable(enable); +} + + +DWORD __stdcall _XInputGetAudioDeviceIds( + DWORD dwUserIndex, + __out_ecount_opt(*pRenderCount) LPWSTR pRenderDeviceId, + __inout_opt UINT* pRenderCount, + __out_ecount_opt(*pCaptureCount) LPWSTR pCaptureDeviceId, + __inout_opt UINT* pCaptureCount) +{ + return g_fpGetAudioDeviceIds ? g_fpGetAudioDeviceIds(dwUserIndex, pRenderDeviceId, pRenderCount, pCaptureDeviceId, pCaptureCount) : ERROR_DEVICE_NOT_CONNECTED; +} + + +DWORD __stdcall _XInputGetBatteryInformation( + DWORD dwUserIndex, + BYTE devType, + __out XINPUT_BATTERY_INFORMATION* pBatteryInformation) +{ + return g_fpGetBatteryInformation ? g_fpGetBatteryInformation(dwUserIndex, devType, pBatteryInformation) : ERROR_DEVICE_NOT_CONNECTED; +} + + +DWORD __stdcall _XInputGetKeystroke( + DWORD dwUserIndex, + __reserved DWORD dwReserved, + __out XINPUT_KEYSTROKE* pKeystroke) +{ + return g_fpGetKeystroke ? g_fpGetKeystroke(dwUserIndex, dwReserved, pKeystroke) : ERROR_DEVICE_NOT_CONNECTED; +} + + +DWORD __stdcall _XInputGetDSoundAudioDeviceGuids( + DWORD dwUserIndex, + __out GUID* pDSoundRenderGuid, + __out GUID* pDSoundCaptureGuid) +{ + return g_fpGetDSoundAudioDeviceGuids ? g_fpGetDSoundAudioDeviceGuids(dwUserIndex, pDSoundRenderGuid, pDSoundCaptureGuid) : ERROR_DEVICE_NOT_CONNECTED; +} + + +} // extern "C" + + +__declspec(dllexport) BOOL __stdcall DllMain([[maybe_unused]] HINSTANCE hinstDLL, DWORD fdwReason, [[maybe_unused]] void* lpvReserved) +{ + constexpr wchar_t pathHagrImplDll[] = L"xinput1_4.dll"; // the DLL which owns Hagr implementation + + if (fdwReason == DLL_PROCESS_ATTACH) + { + if (HMODULE hMod = LoadLibraryW(pathHagrImplDll)) + { + AssignFuncPtr(g_fpGetState, GetProcAddress(hMod, "XInputGetState")); + AssignFuncPtr(g_fpSetState, GetProcAddress(hMod, "XInputSetState")); + AssignFuncPtr(g_fpGetCapabilities, GetProcAddress(hMod, "XInputGetCapabilities")); + AssignFuncPtr(g_fpEnable, GetProcAddress(hMod, "XInputEnable")); + AssignFuncPtr(g_fpGetAudioDeviceIds, GetProcAddress(hMod, "XInputGetAudioDeviceIds")); + AssignFuncPtr(g_fpGetBatteryInformation, GetProcAddress(hMod, "XInputGetBatteryInformation")); + AssignFuncPtr(g_fpGetKeystroke, GetProcAddress(hMod, "XInputGetKeystroke")); + AssignFuncPtr(g_fpGetDSoundAudioDeviceGuids, GetProcAddress(hMod, "XInputGetDSoundAudioDeviceGuids")); + } + } + + return TRUE; +} + + + +// export configuration sets +// x64 uses undecorated names while x86 uses decorated ones + +#if (TARGET_XINPUT_VER_1_3 + TARGET_XINPUT_VER_9_1_0 + TARGET_XINPUT_VER_UAP) != 1 + #error Must define exactly one of the followings: TARGET_XINPUT_VER_1_3=1, TARGET_XINPUT_VER_9_1=1, or TARGET_XINPUT_VER_UAP=1. +#endif + +#if TARGET_XINPUT_VER_1_3 == 1 + #if defined _WIN64 + #pragma comment(linker, "/export:DllMain,@1") + #pragma comment(linker, "/export:XInputGetState=_XInputGetState,@2") + #pragma comment(linker, "/export:XInputSetState=_XInputSetState,@3") + #pragma comment(linker, "/export:XInputGetCapabilities=_XInputGetCapabilities,@4") + #pragma comment(linker, "/export:XInputEnable=_XInputEnable,@5") + #pragma comment(linker, "/export:XInputGetDSoundAudioDeviceGuids=_XInputGetDSoundAudioDeviceGuids,@6") + #pragma comment(linker, "/export:XInputGetBatteryInformation=_XInputGetBatteryInformation,@7") + #pragma comment(linker, "/export:XInputGetKeystroke=_XInputGetKeystroke,@8") + #else + #pragma comment(linker, "/export:_DllMain@12,@1") + #pragma comment(linker, "/export:XInputGetState=__XInputGetState@8,@2") + #pragma comment(linker, "/export:XInputSetState=__XInputSetState@8,@3") + #pragma comment(linker, "/export:XInputGetCapabilities=__XInputGetCapabilities@12,@4") + #pragma comment(linker, "/export:XInputEnable=__XInputEnable@4,@5") + #pragma comment(linker, "/export:XInputGetDSoundAudioDeviceGuids=__XInputGetDSoundAudioDeviceGuids@12,@6") + #pragma comment(linker, "/export:XInputGetBatteryInformation=__XInputGetBatteryInformation@12,@7") + #pragma comment(linker, "/export:XInputGetKeystroke=__XInputGetKeystroke@12,@8") + #endif + +#elif TARGET_XINPUT_VER_9_1_0 == 1 + #if defined _WIN64 + #pragma comment(linker, "/export:DllMain,@1") + #pragma comment(linker, "/export:XInputGetCapabilities=_XInputGetCapabilities,@2") + #pragma comment(linker, "/export:XInputGetDSoundAudioDeviceGuids=_XInputGetDSoundAudioDeviceGuids,@3") + #pragma comment(linker, "/export:XInputGetState=_XInputGetState,@4") + #pragma comment(linker, "/export:XInputSetState=_XInputSetState,@5") + #else + #pragma comment(linker, "/export:_DllMain@12,@1") + #pragma comment(linker, "/export:XInputGetCapabilities=__XInputGetCapabilities@12,@2") + #pragma comment(linker, "/export:XInputGetDSoundAudioDeviceGuids=__XInputGetDSoundAudioDeviceGuids@12,@3") + #pragma comment(linker, "/export:XInputGetState=__XInputGetState@8,@4") + #pragma comment(linker, "/export:XInputSetState=__XInputSetState@8,@5") + #endif + +#elif TARGET_XINPUT_VER_UAP == 1 + #if defined _WIN64 + #pragma comment(linker, "/export:DllMain,@1") + #pragma comment(linker, "/export:XInputEnable=_XInputEnable,@2") + #pragma comment(linker, "/export:XInputGetAudioDeviceIds=_XInputGetAudioDeviceIds,@3") + #pragma comment(linker, "/export:XInputGetBatteryInformation=_XInputGetBatteryInformation,@4") + #pragma comment(linker, "/export:XInputGetCapabilities=_XInputGetCapabilities,@5") + #pragma comment(linker, "/export:XInputGetKeystroke=_XInputGetKeystroke,@6") + #pragma comment(linker, "/export:XInputGetState=_XInputGetState,@7") + #pragma comment(linker, "/export:XInputSetState=_XInputSetState,@8") + #else + #pragma comment(linker, "/export:_DllMain@12,@1") + #pragma comment(linker, "/export:XInputEnable=__XInputEnable@4,@2") + #pragma comment(linker, "/export:XInputGetAudioDeviceIds=__XInputGetAudioDeviceIds@20,@3") + #pragma comment(linker, "/export:XInputGetBatteryInformation=__XInputGetBatteryInformation@12,@4") + #pragma comment(linker, "/export:XInputGetCapabilities=__XInputGetCapabilities@12,@5") + #pragma comment(linker, "/export:XInputGetKeystroke=__XInputGetKeystroke@12,@6") + #pragma comment(linker, "/export:XInputGetState=__XInputGetState@8,@7") + #pragma comment(linker, "/export:XInputSetState=__XInputSetState@8,@8") + #endif + +#endif diff --git a/vcproj/TestMe.vcxproj b/vcproj/TestMe.vcxproj new file mode 100644 index 0000000..b2257e4 --- /dev/null +++ b/vcproj/TestMe.vcxproj @@ -0,0 +1,175 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {4ffe9904-19cf-4ab1-a703-4b5a5b13eab2} + TestMe + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + + + false + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + + + true + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + + + false + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + + + + Level4 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdcpp17 + Async + + + Console + true + false + + + + + Level4 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdcpp17 + Async + + + Console + true + true + true + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdcpp17 + Async + + + Console + true + false + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + stdcpp17 + Async + + + Console + true + true + true + false + + + + + + + + + \ No newline at end of file diff --git a/vcproj/TestMe.vcxproj.filters b/vcproj/TestMe.vcxproj.filters new file mode 100644 index 0000000..f614fc8 --- /dev/null +++ b/vcproj/TestMe.vcxproj.filters @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/vcproj/hagr.vcxproj b/vcproj/hagr.vcxproj new file mode 100644 index 0000000..0df32c5 --- /dev/null +++ b/vcproj/hagr.vcxproj @@ -0,0 +1,222 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + 16.0 + {F10755F3-C007-4D2D-9DAD-C46B42377563} + Win32Proj + hagr + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + xinput1_4 + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + false + + + true + xinput1_4 + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + false + + + false + xinput1_4 + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + false + + + false + xinput1_4 + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + false + + + + Level4 + true + WIN32;_DEBUG;HAGR_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + stdcpp17 + true + true + Async + true + + + Windows + true + false + + + + + + + Level4 + true + _DEBUG;HAGR_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + stdcpp17 + true + true + Async + true + + + Windows + true + false + + + + + + + Level4 + true + true + true + WIN32;NDEBUG;HAGR_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + Default + true + Speed + None + true + Async + true + + + Windows + true + true + false + false + + + + + + + Level4 + true + true + true + NDEBUG;HAGR_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + Default + true + Speed + None + true + Async + true + + + Windows + true + true + false + false + + + + + + + + \ No newline at end of file diff --git a/vcproj/hagr.vcxproj.filters b/vcproj/hagr.vcxproj.filters new file mode 100644 index 0000000..ce3cb9f --- /dev/null +++ b/vcproj/hagr.vcxproj.filters @@ -0,0 +1,61 @@ + + + + + {cb89af00-fb78-4f9d-bef8-1ce4856b30b0} + + + {dc7e67d7-a2bd-4e24-95f3-e38e2b0178fa} + + + + + Controllers + + + Controllers + + + System + + + System + + + System + + + System + + + + System + + + + + Controllers + + + Controllers + + + System + + + System + + + System + + + System + + + System + + + + + + \ No newline at end of file diff --git a/vcproj/hagrStub-1_3.vcxproj b/vcproj/hagrStub-1_3.vcxproj new file mode 100644 index 0000000..a542921 --- /dev/null +++ b/vcproj/hagrStub-1_3.vcxproj @@ -0,0 +1,191 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 16.0 + {2F3E60F4-C508-4557-8F89-6E397D8BC68E} + hagrStub13 + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput1_3 + false + + + true + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput1_3 + false + + + true + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput1_3 + false + + + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput1_3 + false + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_1_3=1 + true + MultiThreaded + None + Speed + true + Async + true + stdcpp17 + + + Windows + true + true + false + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_1_3=1 + true + true + Async + true + stdcpp17 + + + Windows + true + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_1_3=1 + true + true + Async + true + stdcpp17 + + + Windows + true + false + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_1_3=1 + true + MultiThreaded + None + Speed + true + Async + true + stdcpp17 + + + Windows + true + true + false + false + + + + + + \ No newline at end of file diff --git a/vcproj/hagrStub-1_3.vcxproj.filters b/vcproj/hagrStub-1_3.vcxproj.filters new file mode 100644 index 0000000..fc3d1f1 --- /dev/null +++ b/vcproj/hagrStub-1_3.vcxproj.filters @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/vcproj/hagrStub-9_1_0.vcxproj b/vcproj/hagrStub-9_1_0.vcxproj new file mode 100644 index 0000000..2f1c8eb --- /dev/null +++ b/vcproj/hagrStub-9_1_0.vcxproj @@ -0,0 +1,191 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 16.0 + {830344D1-8B8A-4158-82F0-83ACB5E5AC09} + hagrStub13 + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput9_1_0 + false + + + true + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput9_1_0 + false + + + true + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput9_1_0 + false + + + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinput9_1_0 + false + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_9_1_0=1 + true + MultiThreaded + None + Speed + true + Async + true + stdcpp17 + + + Windows + true + true + false + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_9_1_0=1 + true + true + Async + true + stdcpp17 + + + Windows + true + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_9_1_0=1 + true + true + Async + true + stdcpp17 + + + Windows + true + false + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_9_1_0=1 + true + MultiThreaded + None + Speed + true + Async + true + stdcpp17 + + + Windows + true + true + false + false + + + + + + \ No newline at end of file diff --git a/vcproj/hagrStub-9_1_0.vcxproj.filters b/vcproj/hagrStub-9_1_0.vcxproj.filters new file mode 100644 index 0000000..fc3d1f1 --- /dev/null +++ b/vcproj/hagrStub-9_1_0.vcxproj.filters @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/vcproj/hagrStub-uap.vcxproj b/vcproj/hagrStub-uap.vcxproj new file mode 100644 index 0000000..ac4d7cc --- /dev/null +++ b/vcproj/hagrStub-uap.vcxproj @@ -0,0 +1,191 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + 16.0 + {86FDB7D9-7C1A-44D4-9D2C-AECCEA215D0E} + hagrStub13 + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinputuap + false + + + true + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinputuap + false + + + true + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinputuap + false + + + false + $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(SolutionDir)\obj\$(ProjectName)\$(Platform)\$(Configuration)\ + xinputuap + false + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_UAP=1 + true + MultiThreaded + None + Speed + true + Async + true + stdcpp17 + + + Windows + true + true + false + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_UAP=1 + true + true + Async + true + stdcpp17 + + + Windows + true + false + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_UAP=1 + true + true + Async + true + stdcpp17 + + + Windows + true + false + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);TARGET_XINPUT_VER_UAP=1 + true + MultiThreaded + None + Speed + true + Async + true + stdcpp17 + + + Windows + true + true + false + false + + + + + + \ No newline at end of file diff --git a/vcproj/hagrStub-uap.vcxproj.filters b/vcproj/hagrStub-uap.vcxproj.filters new file mode 100644 index 0000000..fc3d1f1 --- /dev/null +++ b/vcproj/hagrStub-uap.vcxproj.filters @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file