From 59021f703e0577b90f803b0394b47f13185dd291 Mon Sep 17 00:00:00 2001 From: Adrian Bibby Walther Date: Sun, 4 Aug 2019 17:00:25 +0200 Subject: [PATCH] Now supports binding to two directions at once. In the process there has been a lot of cleaning. --- CMakeLists.txt | 12 +- Dish2Macro.cfg => Dish2Macro.txt | 1 - LICENSE | 30 ++--- README.md | 44 ++++++-- main.cpp | 181 ++++++++++++++++++++++--------- 5 files changed, 187 insertions(+), 81 deletions(-) rename Dish2Macro.cfg => Dish2Macro.txt (50%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f14b50a..4d398a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ -cmake_minimum_required(VERSION 3.0.0) -project(Dish2Macro VERSION 1.3.0 LANGUAGES CXX) +cmake_minimum_required(VERSION 3.12.0) +project(Dish2Macro VERSION 1.4.0 LANGUAGES CXX) add_executable(Dish2Macro main.cpp) @@ -8,3 +8,11 @@ target_compile_features(Dish2Macro PRIVATE cxx_std_17) if(MSVC) target_compile_definitions(Dish2Macro PRIVATE _CRT_SECURE_NO_WARNINGS) endif() + +install(TARGETS Dish2Macro + RUNTIME DESTINATION . +) + +install(FILES Dish2Macro.txt + DESTINATION . +) diff --git a/Dish2Macro.cfg b/Dish2Macro.txt similarity index 50% rename from Dish2Macro.cfg rename to Dish2Macro.txt index 0ebb6b8..a1b07c7 100644 --- a/Dish2Macro.cfg +++ b/Dish2Macro.txt @@ -1,2 +1 @@ 0x20 -Down diff --git a/LICENSE b/LICENSE index da45047..72d44be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,15 +1,15 @@ -ISC License - -Copyright (c) 2017, Som1Lse - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +ISC License + +Copyright (c) 2017, 2019, Adrian Bibby Walther + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index f194b25..a90b654 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,35 @@ -A macro for Dishonored 2, that allows you to bunnyhop without a freescroll mouse. - -If you don't like spacebar you can change the number in `Dish2Macro.cfg` to a different key code. For a list of these key codes look [here](https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx). - -Oh, and don't forget to bind `wheel down` to jump in Dishonored 2. - -# Is this allowed in speedruns? - -Yes. +A macro for Dishonored 2 (and since then also Dishonored: Death of the Outsider and Dishonored 1), +that allows you to bunnyhop without a freescroll mouse. + +Oh, and don't forget to bind `wheel down` to jump in game. + +# Dish2Macro.txt + +If you don't like using `space` you can change it in `Dish2Macro.txt` to a different key code. + +For a list of these key codes see [here](https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx). + +Here are a few sample configuration files: + + - Bind `space` to spamming `wheel down`. + - `32` or + - `0x20` or + - `0x20 Down` or + - `0x20 0x0` + - Bind `space` to spamming `wheel up`. + - `0x20 Up` or + - `0x0 0x20` + - Bind `middle mouse button` (pressing the wheel itself) to spamming `wheel down`. (This is my preferred setup for Dishonored 2/DotO.) + - `0x4 Down` + - Bind `G` to spamming `wheel down`. (This is my preferred setup for Dishonored 1.) + - `0x47 Down` + - Bind `G` to spamming `wheel down`, and `H` to spamming `wheel up`. + - `0x47 0x48` + +and so on... + +# Is this allowed in speedruns? + +Yes. + +Specifically any of the Dishonored games allow it. diff --git a/main.cpp b/main.cpp index 1095b1d..22f09af 100644 --- a/main.cpp +++ b/main.cpp @@ -1,24 +1,29 @@ #include +#include #include #include #include -#include #include #include +#include #include using namespace std::literals; +namespace { + // Global variables are necessary, because Windows hooks have a terrible API. -HHOOK Hook; -std::atomic ShouldJump(false); +HHOOK MouseHook; +HHOOK KeyboardHook; -DWORD KeyCode = VK_SPACE; +constexpr unsigned SpamDownBit = 0x1; +constexpr unsigned SpamUpBit = 0x2; +std::atomic SpamFlags(0); -DWORD WheelDelta = -WHEEL_DELTA; -DWORD WheelEvent = MOUSEEVENTF_WHEEL; +DWORD DownKeyCode = 0; +DWORD UpKeyCode = 0; bool IsGameInFocus(){ auto Window = GetForegroundWindow(); @@ -50,14 +55,36 @@ bool IsGameInFocus(){ return true; } +bool HandleKey(DWORD KeyCode,bool IsDown){ + unsigned Mask = 0; + + if(KeyCode == DownKeyCode){ + Mask = SpamDownBit; + }else if(KeyCode == UpKeyCode){ + Mask = SpamUpBit; + } + + if(Mask == 0 || !IsGameInFocus()){ + return false; + } + + if(IsDown){ + SpamFlags |= Mask; + }else{ + SpamFlags &= ~Mask; + } + + return true; +} + LRESULT CALLBACK LowLevelMouseProc(int Code,WPARAM WParam,LPARAM LParam){ if(Code < HC_ACTION){ - return CallNextHookEx(Hook,Code,WParam,LParam); + return CallNextHookEx(MouseHook,Code,WParam,LParam); } assert(Code == HC_ACTION); - unsigned ButtonCode = 0; + DWORD KeyCode = 0; bool IsDown = false; switch(WParam){ case WM_LBUTTONDOWN: { @@ -66,7 +93,7 @@ LRESULT CALLBACK LowLevelMouseProc(int Code,WPARAM WParam,LPARAM LParam){ [[fallthrough]]; } case WM_LBUTTONUP: { - ButtonCode = VK_LBUTTON; + KeyCode = VK_LBUTTON; break; } case WM_RBUTTONDOWN: { @@ -75,7 +102,7 @@ LRESULT CALLBACK LowLevelMouseProc(int Code,WPARAM WParam,LPARAM LParam){ [[fallthrough]]; } case WM_RBUTTONUP: { - ButtonCode = VK_RBUTTON; + KeyCode = VK_RBUTTON; break; } case WM_MBUTTONDOWN: { @@ -84,7 +111,7 @@ LRESULT CALLBACK LowLevelMouseProc(int Code,WPARAM WParam,LPARAM LParam){ [[fallthrough]]; } case WM_MBUTTONUP: { - ButtonCode = VK_MBUTTON; + KeyCode = VK_MBUTTON; break; } case WM_XBUTTONDOWN: { @@ -94,24 +121,22 @@ LRESULT CALLBACK LowLevelMouseProc(int Code,WPARAM WParam,LPARAM LParam){ } case WM_XBUTTONUP: { auto& Info = *reinterpret_cast(LParam); + KeyCode = VK_XBUTTON1+HIWORD(Info.mouseData)-XBUTTON1; - ButtonCode = VK_XBUTTON1+HIWORD(Info.mouseData)-XBUTTON1; break; } } - if(ButtonCode == KeyCode && IsGameInFocus()){ - ShouldJump = IsDown; - - return 1;// Stop propagation. + if(HandleKey(KeyCode,IsDown)){ + return 1; } - return CallNextHookEx(Hook,Code,WParam,LParam); + return CallNextHookEx(MouseHook,Code,WParam,LParam); } LRESULT CALLBACK LowLevelKeyboardProc(int Code,WPARAM WParam,LPARAM LParam){ if(Code < HC_ACTION){ - return CallNextHookEx(Hook,Code,WParam,LParam); + return CallNextHookEx(KeyboardHook,Code,WParam,LParam); } assert(Code == HC_ACTION); @@ -120,31 +145,37 @@ LRESULT CALLBACK LowLevelKeyboardProc(int Code,WPARAM WParam,LPARAM LParam){ auto IsDown = (WParam == WM_KEYDOWN || WParam == WM_SYSKEYDOWN); - if(Info.vkCode == KeyCode && IsGameInFocus()){ - ShouldJump = IsDown; - - return 1;// Stop propagation. + if(HandleKey(Info.vkCode,IsDown)){ + return 1; } - return CallNextHookEx(Hook,Code,WParam,LParam); + return CallNextHookEx(KeyboardHook,Code,WParam,LParam); } -void SendJump(){ - mouse_event(WheelEvent,0,0,WheelDelta,0); -} +DWORD GetWheelDelta(unsigned Flags){ + DWORD r = 0; -void CALLBACK TimerProc(void*,BOOLEAN){ - if(!ShouldJump){ - return; + if((Flags&SpamDownBit) != 0){ + r -= WHEEL_DELTA; } - if(!IsGameInFocus()){ - ShouldJump = false; - - return; + if((Flags&SpamUpBit) != 0){ + r += WHEEL_DELTA; } - SendJump(); + return r; +} + +void CALLBACK TimerProc(void*,BOOLEAN){ + if(IsGameInFocus()){ + auto WheelDelta = GetWheelDelta(SpamFlags); + + if(WheelDelta != 0){ + mouse_event(MOUSEEVENTF_WHEEL,0,0,WheelDelta,0); + } + }else{ + SpamFlags = 0; + } } struct file_deleter { @@ -155,30 +186,72 @@ struct file_deleter { using unique_file = std::unique_ptr; +// There are a few valid possible configurations: +// : is the scroll down key. +// : is the key for either scroll up or down depending on whether or not is `u` or not. +// : is the scroll down key. is the scroll up key. void ReadConfiguration(const char* Filename){ unique_file File(std::fopen(Filename,"rb")); if(!File){ throw std::runtime_error("Unable to open configuration file \""s+Filename+"\"."); } - char WheelDir = 'd'; + auto KeyCodesRead = std::fscanf(File.get(),"%li %li",&DownKeyCode,&UpKeyCode); - if(std::fscanf(File.get(),"0x%x %c",&KeyCode,&WheelDir) < 1 && std::fscanf(File.get(),"%u %c",&KeyCode,&WheelDir) < 1){ + if(KeyCodesRead < 1){ throw std::runtime_error("Unable to read key code from configuration file \""s+Filename+"\"."); } - // `|32` convers ASCII letters to lower case. - // None of the Dishonored games support left or right scrolling, so there is no point to supporting them here. - switch(WheelDir|32){ - case 'u': WheelDelta = WHEEL_DELTA; break; - case 'd': WheelDelta = -WHEEL_DELTA; break; - default: throw std::runtime_error("Invalid wheel direction in configuration file"s+Filename+"\"."); + if(KeyCodesRead < 2){ + char WheelDirection = 'd'; + + if(std::fscanf(File.get()," %c",&WheelDirection) == 1){ + // `|32` converts ASCII letters to lower case. + switch(WheelDirection|32){ + case 'd': break; + case 'u': { + UpKeyCode = DownKeyCode; + DownKeyCode = 0; + + break; + } + default: { + throw std::runtime_error("Invalid wheel direction '+"s+WheelDirection+"+' in configuration file \""+Filename+"\"."); + } + } + } + } + + if(DownKeyCode > VK_OEM_CLEAR){ + throw std::runtime_error("Invalid key code "+std::to_string(DownKeyCode)+" in configuration file \""s+Filename+"\"."); + } + + if(UpKeyCode > VK_OEM_CLEAR){ + throw std::runtime_error("Invalid key code "+std::to_string(UpKeyCode)+" in configuration file \""s+Filename+"\"."); + } + + if(DownKeyCode == UpKeyCode){ + if(DownKeyCode == 0){ + throw std::runtime_error("No key was bound to either scroll direction in configuration file \""s+Filename+"\"."); + }else{ + throw std::runtime_error("Scroll up and scroll down both have the same binding in configuration file \""s+Filename+"\"."); + } } } +bool IsMouseButton(DWORD KeyCode){ + return (VK_LBUTTON <= KeyCode && KeyCode <= VK_RBUTTON) || (VK_MBUTTON <= KeyCode && KeyCode <= VK_XBUTTON2); +} + +bool IsKeyboardKey(DWORD KeyCode){ + return KeyCode != 0 && !IsMouseButton(KeyCode); +} + +} + int main(int argc,char** argv){ try { - auto ConfigFilename = "Dish2Macro.cfg"; + auto ConfigFilename = "Dish2Macro.txt"; if(argc >= 2){ ConfigFilename = argv[1]; @@ -186,18 +259,20 @@ int main(int argc,char** argv){ ReadConfiguration(ConfigFilename); - auto IsMouseButton = - (KeyCode >= VK_LBUTTON && KeyCode <= VK_RBUTTON) || - (KeyCode >= VK_MBUTTON && KeyCode <= VK_XBUTTON2); + if(IsMouseButton(DownKeyCode) || IsMouseButton(UpKeyCode)){ + MouseHook = SetWindowsHookExW(WH_MOUSE_LL,LowLevelMouseProc,nullptr,0); - if(IsMouseButton){ - Hook = SetWindowsHookExW(WH_MOUSE_LL,LowLevelMouseProc,nullptr,0); - }else{ - Hook = SetWindowsHookExW(WH_KEYBOARD_LL,LowLevelKeyboardProc,nullptr,0); + if(!MouseHook){ + throw std::system_error(GetLastError(),std::system_category()); + } } - if(!Hook){ - throw std::system_error(GetLastError(),std::system_category()); + if(IsKeyboardKey(DownKeyCode) || IsKeyboardKey(UpKeyCode)){ + KeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL,LowLevelKeyboardProc,nullptr,0); + + if(!KeyboardHook){ + throw std::system_error(GetLastError(),std::system_category()); + } } // Execute every 5 milliseconds, 200 times a second. @@ -221,6 +296,4 @@ int main(int argc,char** argv){ return 1; } - - return 0; }