diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..90bc9a4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: Build and Release TouchJoy + +on: + push: + branches: [ "master", "main" ] + pull_request: + branches: [ "master", "main" ] + workflow_dispatch: + +jobs: + build-windows: + runs-on: windows-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Configure CMake + run: cmake -B build -S . + + - name: Compile Project + run: cmake --build build --config Release + + - name: Package Artifacts + shell: bash + run: | + mkdir -p TouchJoy-Release + cp build/Release/TouchJoy.exe TouchJoy-Release/ + cp data/* TouchJoy-Release/ + mv TouchJoy-Release/sample.ini TouchJoy-Release/config.ini + + - name: Upload Downloadable Artifact + uses: actions/upload-artifact@v4 + with: + name: TouchJoy-Windows + path: TouchJoy-Release/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8f248e4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.15) +project(TouchJoy C CXX) + +set(CMAKE_C_STANDARD 99) + +# Fetch ViGEmClient automatically for Xbox 360 controller emulation +include(FetchContent) +FetchContent_Declare( + ViGEmClient + GIT_REPOSITORY https://github.com/nefarius/ViGEmClient.git + GIT_TAG master +) +FetchContent_MakeAvailable(ViGEmClient) + +add_compile_definitions( + _CRT_SECURE_NO_WARNINGS + GB_INI_MAX_SECTION_LENGTH=16 + GB_INI_MAX_NAME_LENGTH=16 +) + +# 1. Main Windowed Application +add_executable(TouchJoy WIN32 + src/gamepad.c + src/gamepad_window.c + src/libs.c + src/main.c + src/utils.c +) +# Link ViGEm, Windows APIs, and Shell32 (for the browser prompt) +target_link_libraries(TouchJoy PRIVATE ViGEmClient setupapi.lib winmm.lib shell32.lib) + +# 2. Test Console Application +add_executable(TouchJoyTest + src/gamepad.c + src/gamepad_window.c + src/libs.c + src/main.c + src/utils.c +) +target_compile_definitions(TouchJoyTest PRIVATE _TEST) +target_link_libraries(TouchJoyTest PRIVATE ViGEmClient setupapi.lib winmm.lib shell32.lib) \ No newline at end of file diff --git a/README.md b/README.md index 2c5e0ef..89d746e 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,42 @@ -TouchJoy - On screen gamepad for touch-screen Windows devices +TouchJoy - On-screen gamepad for touch-screen Windows devices ============================================================= ## What is it? -This program helps you create an on-screen gamepad so that you can game on a touch-enabled Windows device: +This program creates a highly customizable on-screen virtual gamepad so you can play modern PC games on a touch-enabled Windows device (like a Microsoft Surface, ROG Ally, or Windows tablet). ![screenshot](data/screenshot.jpg) -The layout is fully customizable using a simple [ini file](data/sample.ini). -The file is also automatically reloaded whenever it is saved so that you can tweak and adjust it quickly. +Unlike legacy mappers that just send fake keyboard presses, **TouchJoy natively emulates an Xbox 360 Controller**. This means it is instantly recognized by modern games via XInput with full analog joystick and trigger support. -## Why? +The layout is fully customizable using a simple `config.ini` file. The file is automatically reloaded whenever it is saved, so you can tweak and adjust your layout in real-time without restarting the app. -I want to play Windows games lying down and there are not many good on-screen gamepad applications for Windows. -It is also a chance to learn how far I can go with only C99 and as few external libraries as possible. +## Prerequisites -## How to build? +Because TouchJoy emulates a physical hardware controller, it requires the **ViGEmBus** kernel-mode driver to be installed on your system. -You need [GENie](https://github.com/bkaradzic/genie) and Visual Studio 2013+ since the project is written in C99. +* If you do not have it installed, TouchJoy will automatically prompt you on startup and open your browser to the [Official ViGEmBus GitHub Releases](https://github.com/nefarius/ViGEmBus/releases/latest) page so you can download it safely. -`vs2013.bat` can be used to generate a solution for Visual Studio 2013. +## How to install and use? -## How to use? +**The easy way:** +1. Go to the **Actions** or **Releases** tab on this GitHub repository. +2. Download the latest `TouchJoy-Windows.zip` artifact. +3. Extract the folder anywhere on your PC. +4. Double click `TouchJoy.exe`. +5. To change your layout or map different buttons, open `config.ini` in any text editor. (For now, the code and `config.ini` itself are the best manuals). -For now, the code and [this sample](data/sample.ini) are the only manuals. -I'm terribly sorry. +## How to build from source? -## External libraries +The project has been modernized to use **CMake**. It requires Visual Studio 2019/2022 (with the Desktop C++ workload) or any modern C99-compliant compiler. -* [stb_image](https://github.com/nothings/stb): image loading -* [gb_ini](https://github.com/gingerBill/gb): ini parsing -* [utest](https://github.com/evolutional/utest): unit testing +```bash +# Clone the repository +git clone [https://github.com/YOUR-USERNAME/TouchJoy.git](https://github.com/YOUR-USERNAME/TouchJoy.git) +cd TouchJoy -Special thanks to Sean Barrett for providing an useful [list](https://github.com/nothings/stb/blob/master/docs/other_libs.md) of single-header libraries. +# Configure the build system (this will automatically fetch ViGEmClient) +cmake -B build -S . + +# Compile the project +cmake --build build --config Release diff --git a/genie.lua b/genie.lua deleted file mode 100644 index 3bff772..0000000 --- a/genie.lua +++ /dev/null @@ -1,60 +0,0 @@ -solution "touch-joy" - location(_ACTION) - configurations {"Develop"} - platforms {"x64"} - targetdir "bin" - debugdir "data" - - project "touch-joy" - kind "WindowedApp" - language "C" - - defines { - "_CRT_SECURE_NO_WARNINGS", - "GB_INI_MAX_SECTION_LENGTH=16", - "GB_INI_MAX_NAME_LENGTH=16" - } - - files { - "src/*.h", - "src/*.c" - } - - flags { - "ExtraWarnings", - "FatalWarnings", - "OptimizeSize", - "StaticRuntime", - "Symbols", - "WinMain", - "NoEditAndContinue", - "NoNativeWChar", - "NoExceptions", - "NoFramePointer" - } - - project "test" - kind "ConsoleApp" - language "C" - - defines { - "_CRT_SECURE_NO_WARNINGS", - "_TEST", - "GB_INI_MAX_SECTION_LENGTH=16", - "GB_INI_MAX_NAME_LENGTH=16" - } - - files { - "src/*.h", - "src/*.c" - } - - flags { - "FatalWarnings", - "OptimizeSize", - "StaticRuntime", - "Symbols", - "NoEditAndContinue", - "NoNativeWChar", - "NoExceptions" - } diff --git a/sample.bat b/sample.bat deleted file mode 100644 index f2800c8..0000000 --- a/sample.bat +++ /dev/null @@ -1 +0,0 @@ -bin\touch-joy.exe data\sample.ini diff --git a/src/gamepad_window.c b/src/gamepad_window.c index 7a38bd9..a00fc37 100644 --- a/src/gamepad_window.c +++ b/src/gamepad_window.c @@ -4,6 +4,9 @@ #define VC_EXTRALEAN #include #include +#include // Required for _stricmp + +#include #include "utils.h" @@ -11,6 +14,11 @@ #define BUTTON(HWND, VAR) \ Button* VAR = (Button*)GetWindowLongPtr(HWND, GWLP_USERDATA); +// EXTERN VIGEM STATE +extern PVIGEM_CLIENT g_client; +extern PVIGEM_TARGET g_pad; +extern XUSB_REPORT g_report; + typedef enum { TOUCH_DOWN, @@ -53,6 +61,54 @@ void HandleKeyButton(Button* button, bool down) SendInput(1, &input, sizeof(INPUT)); } +// --- NEW XBOX BUTTON MAPPING --- +USHORT GetXboxButtonMask(const char* name) +{ + if (_stricmp(name, "a") == 0) return XUSB_GAMEPAD_A; + if (_stricmp(name, "b") == 0) return XUSB_GAMEPAD_B; + if (_stricmp(name, "x") == 0) return XUSB_GAMEPAD_X; + if (_stricmp(name, "y") == 0) return XUSB_GAMEPAD_Y; + if (_stricmp(name, "lb") == 0 || _stricmp(name, "l") == 0) return XUSB_GAMEPAD_LEFT_SHOULDER; + if (_stricmp(name, "rb") == 0 || _stricmp(name, "r") == 0) return XUSB_GAMEPAD_RIGHT_SHOULDER; + if (_stricmp(name, "start") == 0) return XUSB_GAMEPAD_START; + if (_stricmp(name, "select") == 0 || _stricmp(name, "back") == 0) return XUSB_GAMEPAD_BACK; + if (_stricmp(name, "up") == 0) return XUSB_GAMEPAD_DPAD_UP; + if (_stricmp(name, "down") == 0) return XUSB_GAMEPAD_DPAD_DOWN; + if (_stricmp(name, "left") == 0) return XUSB_GAMEPAD_DPAD_LEFT; + if (_stricmp(name, "right") == 0) return XUSB_GAMEPAD_DPAD_RIGHT; + if (_stricmp(name, "ls") == 0 || _stricmp(name, "l3") == 0) return XUSB_GAMEPAD_LEFT_THUMB; + if (_stricmp(name, "rs") == 0 || _stricmp(name, "r3") == 0) return XUSB_GAMEPAD_RIGHT_THUMB; + return 0; +} + +void HandleXboxButton(Button* button, bool down) +{ + // Handle Analog Triggers (LT / RT) + if (_stricmp(button->name, "lt") == 0) { + g_report.bLeftTrigger = down ? 255 : 0; + } + else if (_stricmp(button->name, "rt") == 0) { + g_report.bRightTrigger = down ? 255 : 0; + } + else { + // Handle Digital Buttons + USHORT mask = GetXboxButtonMask(button->name); + if (mask != 0) { + if (down) g_report.wButtons |= mask; + else g_report.wButtons &= ~mask; + } + else { + // Fallback: If it's not a standard Xbox name, send a normal keyboard key + HandleKeyButton(button, down); + return; + } + } + + // Push the updated state to the virtual controller + vigem_target_x360_update(g_client, g_pad, g_report); +} +// ------------------------------- + void HandleQuitButton(Button* button, bool down) { UNUSED(button); @@ -64,17 +120,11 @@ void HandleWheelButton(Button* button, bool down) { if (!down) { return; } - // Scrolling is a bit tricky. - // First we need to move the mouse to the target area. - // Then we simulate a scroll event. INPUT inputs[2]; - // Move the mouse to a point slightly above and to the left of the button's - // top left corner inputs[0].type = INPUT_MOUSE; int x = GetButtonX(button) - 5; int y = GetButtonY(button) - 5; - // Windows uses a weird coordinate system for mouse: [0, 65535] int absX = (int)((float)x / (float)GetSystemMetrics(SM_CXSCREEN) * 65535.f); int absY = (int)((float)y / (float)GetSystemMetrics(SM_CYSCREEN) * 65535.f); inputs[0].mi.dx = absX; @@ -84,7 +134,6 @@ void HandleWheelButton(Button* button, bool down) inputs[0].mi.time = 0; inputs[0].mi.dwExtraInfo = 0; - // Scroll inputs[1].type = INPUT_MOUSE; inputs[1].mi.dx = 0; inputs[1].mi.dy = 0; @@ -98,50 +147,24 @@ void HandleWheelButton(Button* button, bool down) void HandleStickButton(Button* button, TouchEvent event, int touchX, int touchY) { - float joyX, joyY; + float joyX = 0.0f; + float joyY = 0.0f; - if (event == TOUCH_UP) - { - // If the touch is released, the stick moves to its center position - joyX = 0.f; - joyY = 0.f; - } - else + if (event != TOUCH_UP) { - // In other cases, use the real touch position to calculate stick - // position + joyX = (float)touchX / (float)button->width * 2.0f - 1.0f; + joyY = (float)touchY / (float)button->height * 2.0f - 1.0f; - joyX = (float)touchX / (float)button->width * 2.f - 1.f; - joyY = (float)touchY / (float)button->height * 2.f - 1.f; + if (joyX < -1.0f) joyX = -1.0f; + if (joyX > 1.0f) joyX = 1.0f; + if (joyY < -1.0f) joyY = -1.0f; + if (joyY > 1.0f) joyY = 1.0f; } - bool newStates[4]; - float threshold = button->extras.stick.threshold; - newStates[STICK_UP] = joyY < -threshold; - newStates[STICK_DOWN] = joyY > threshold; - newStates[STICK_LEFT] = joyX < -threshold; - newStates[STICK_RIGHT] = joyX > threshold; - - INPUT inputs[4]; - int numInputs = 0; - - for (int i = 0; i < 4; ++i) - { - if (newStates[i] != button->extras.stick.states[i]) - { - INPUT* input = &inputs[numInputs++]; - input->type = INPUT_KEYBOARD; - input->ki.wVk = button->extras.stick.codes[i]; - input->ki.dwFlags = newStates[i] ? 0 : KEYEVENTF_KEYUP; - input->ki.wScan = 0; - input->ki.time = 0; - input->ki.dwExtraInfo = 0; - } - - button->extras.stick.states[i] = newStates[i]; - } + g_report.sThumbLX = (SHORT)(joyX * 32767.0f); + g_report.sThumbLY = (SHORT)(joyY * -32767.0f); - SendInput(numInputs, inputs, sizeof(INPUT)); + vigem_target_x360_update(g_client, g_pad, g_report); } void HandleUpDown(Button* button, bool down) @@ -149,7 +172,8 @@ void HandleUpDown(Button* button, bool down) switch (button->type) { case BTN_KEY: - HandleKeyButton(button, down); + // Route through our new Xbox Button handler! + HandleXboxButton(button, down); break; case BTN_WHEEL: HandleWheelButton(button, down); @@ -163,7 +187,7 @@ void HandleUpDown(Button* button, bool down) LRESULT CALLBACK OnTouch(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { BUTTON(hWnd, button); - TOUCHINPUT touch; //Only handle the first touch on any button + TOUCHINPUT touch; if (GetTouchInputInfo((HTOUCHINPUT)lParam, 1, &touch, sizeof(TOUCHINPUT))) { diff --git a/src/main.c b/src/main.c index 9072e53..2f3b5af 100644 --- a/src/main.c +++ b/src/main.c @@ -5,6 +5,9 @@ #define WIN32_LEAN_AND_MEAN #define VC_EXTRALEAN #include +#include // Required for ShellExecute (opening the browser) + +#include #include "gamepad.h" #include "gamepad_window.h" @@ -12,6 +15,11 @@ #define WM_CONFIGCHANGED (WM_USER + 1) +// Global Xbox Controller State +PVIGEM_CLIENT g_client; +PVIGEM_TARGET g_pad; +XUSB_REPORT g_report; + typedef struct { Gamepad gamepad; @@ -127,6 +135,35 @@ int CALLBACK WinMain( return -1; } + // --- VIGEM INITIALIZATION --- + g_client = vigem_alloc(); + if (g_client == NULL) { + MessageBox(NULL, "Not enough memory for ViGEmClient", "Error", MB_OK); + return -1; + } + + VIGEM_ERROR verr = vigem_connect(g_client); + if (!VIGEM_SUCCESS(verr)) { + int response = MessageBox(NULL, + "Could not connect to ViGEmBus.\n\nThe virtual gamepad driver is not installed. Would you like to open your browser to download the official installer now?", + "ViGEmBus Driver Required", + MB_YESNO | MB_ICONWARNING | MB_TOPMOST); + + if (response == IDYES) { + // Opens the user's default browser to the official ViGEmBus download page + ShellExecute(NULL, "open", "https://github.com/nefarius/ViGEmBus/releases/latest", NULL, NULL, SW_SHOWNORMAL); + } + + // Clean up the memory we allocated before closing + vigem_free(g_client); + return -1; + } + + g_pad = vigem_target_x360_alloc(); + vigem_target_add(g_client, g_pad); + XUSB_REPORT_INIT(&g_report); + // ---------------------------- + // Display gamepad RegisterGamepadWindowClass(); InitializeGamepad(&state.gamepad); @@ -172,6 +209,13 @@ int CALLBACK WinMain( DeinitializeGamepad(&state.gamepad); FreeGamepad(&state.gamepad); + // --- VIGEM CLEANUP --- + vigem_target_remove(g_client, g_pad); + vigem_target_free(g_pad); + vigem_disconnect(g_client); + vigem_free(g_client); + // --------------------- + return (int)msg.wParam; } diff --git a/vs2022.bat b/vs2022.bat deleted file mode 100644 index 73852fb..0000000 --- a/vs2022.bat +++ /dev/null @@ -1 +0,0 @@ -genie vs2022 \ No newline at end of file