Skip to content

Instantly share code, notes, and snippets.

@ryrun
Created November 23, 2024 13:51
Show Gist options
  • Save ryrun/3e431a1701f9da196be1ff5390994736 to your computer and use it in GitHub Desktop.
Save ryrun/3e431a1701f9da196be1ff5390994736 to your computer and use it in GitHub Desktop.
Changing Shadow PC App Gamepad Button Mapping with SDL2 on macOS using SDL_GAMECONTROLLERCONFIG

Changing Shadow PC App Gamepad Button Mapping with SDL2 on macOS

The Shadow PC app uses SDL2 for gamepad recognition. This has the advantage that you can change the button mapping of recognized controllers. You can add a custom configuration for each SDL2 application using the SDL_GAMECONTROLLERCONFIG environment variable. The format is relatively simple:

GUID,Name,Mapping

Currently, I am having issues with Xbox 360 controllers (a Xim Matrix which is using the PC XInput mode) on macOS 15, as they are recognized twice in the Shadow PC app with SDL version 2.24.2 (as of Nov 23, 2024). The problem is described here: GitHub Issue #11002. Since I do not want to wait too long for an update, I use the following mapping to "break" the 360 controller, so no duplicate inputs are sent to Shadow PC:

030000005e0400008e02000014016800,Dead360,leftx:a6,lefty:a7,rightx:a8,righty:a9,platform:Mac OS X

To get the correct GUID of the duplicate controllers so that I can disable the faulty one, I wrote my own tool. For this, the correct SDL2 framework must be installed on macOS. I followed this guide: https://github.com/rosejoshua/QuickSDL2Mac

With the help of ChatGPT, I built a test controller app that also reads the mapping config on Shadow. This allowed me to test how SDL2 applications respond and whether my mapping works in the end:

#include <SDL2/SDL.h>
#include <iostream>
#include <fstream>
#include <string>

int main(int argc, char* argv[]) {
    if (SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS) != 0) {
        std::cerr << "SDL could not be initialized: " << SDL_GetError() << std::endl;
        return 1;
    }

    // Load the gamecontroller mapping file
    std::string mappingFilePath = "/Applications/Shadow PC.app/Contents/Resources/app.asar.unpacked/release/native/ShadowPCDisplay.app/Contents/Resources/gamecontrollerdb.txt";
    int mappingsAdded = SDL_GameControllerAddMappingsFromFile(mappingFilePath.c_str());
    if (mappingsAdded == -1) {
        std::cerr << "Error loading gamecontroller mapping file: " << SDL_GetError() << std::endl;
    } else {
        std::cout << "Gamecontroller mapping file successfully loaded. Number of mappings added: " << mappingsAdded << std::endl;
    }

    int numJoysticks = SDL_NumJoysticks();
    for (int i = 0; i < numJoysticks; ++i) {
        if (SDL_IsGameController(i)) {
            SDL_GameController* controller = SDL_GameControllerOpen(i);
            if (controller) {
                SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
                SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
                char guidStr[33];
                SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
                std::cout << "Controller " << i << " connected: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << std::endl;
                if (SDL_GameControllerMapping(controller)) {
                    std::cout << "  Mapping: " << SDL_GameControllerMapping(controller) << std::endl;
                } else {
                    std::cout << "  No valid mapping information available." << std::endl;
                }
            }
        }
    }

    bool running = true;
    SDL_Event event;

    while (running) {
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_QUIT:
                    running = false;
                    break;
                case SDL_CONTROLLERDEVICEADDED: {
                    SDL_GameController* controller = SDL_GameControllerOpen(event.cdevice.which);
                    if (controller) {
                        SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
                        SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
                        char guidStr[33];
                        SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
                        std::cout << "New controller connected: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << std::endl;
                        if (SDL_GameControllerMapping(controller)) {
                            std::cout << "  Mapping: " << SDL_GameControllerMapping(controller) << std::endl;
                        } else {
                            std::cout << "  No valid mapping information available." << std::endl;
                        }
                    }
                    break;
                }
                case SDL_CONTROLLERDEVICEREMOVED:
                    std::cout << "Controller removed: Instance ID " << event.cdevice.which << std::endl;
                    break;
                case SDL_CONTROLLERBUTTONDOWN:
                case SDL_CONTROLLERBUTTONUP: {
                    SDL_GameController* controller = SDL_GameControllerFromInstanceID(event.cbutton.which);
                    if (controller) {
                        SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
                        SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
                        char guidStr[33];
                        SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
                        std::cout << "Controller Button Event: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << " Button: " << static_cast<int>(event.cbutton.button) << " Status: " << (event.type == SDL_CONTROLLERBUTTONDOWN ? "pressed" : "released") << std::endl;
                    }
                    break;
                }
                case SDL_CONTROLLERAXISMOTION: {
                    SDL_GameController* controller = SDL_GameControllerFromInstanceID(event.caxis.which);
                    if (controller) {
                        SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller);
                        SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
                        char guidStr[33];
                        SDL_JoystickGetGUIDString(guid, guidStr, sizeof(guidStr));
                        const char* axisName;
                        switch (event.caxis.axis) {
                            case SDL_CONTROLLER_AXIS_LEFTX:
                                axisName = "Left Stick X";
                                break;
                            case SDL_CONTROLLER_AXIS_LEFTY:
                                axisName = "Left Stick Y";
                                break;
                            case SDL_CONTROLLER_AXIS_RIGHTX:
                                axisName = "Right Stick X";
                                break;
                            case SDL_CONTROLLER_AXIS_RIGHTY:
                                axisName = "Right Stick Y";
                                break;
                            default:
                                axisName = "Unknown Axis";
                                break;
                        }
                        std::cout << "Controller Axis Event: " << SDL_GameControllerName(controller) << " GUID: " << guidStr << " " << axisName << " Value: " << event.caxis.value << std::endl;
                    }
                    break;
                }
            }
        }
    }

    SDL_Quit();
    return 0;
}

Permanently Set the Environment Variable SDL_GAMECONTROLLERCONFIG

To ensure that every macOS application and Terminal has this environment variable set, you need to create a .plist file in ~/Library/LaunchAgents/. I named it com.user.sdlcontroller.plist.

The content of the .plist file is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.sdlcontroller</string>
    <key>ProgramArguments</key>
    <array>
        <string>launchctl</string>
        <string>setenv</string>
        <string>SDL_GAMECONTROLLERCONFIG</string>
        <string>030000005e0400008e02000014016800,Dead360,leftx:a6,lefty:a7,rightx:a8,righty:a9,platform:Mac OS X</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

Then, the .plist file needs to be loaded once using launchctl. This can be done via Terminal:

launchctl load ~/Library/LaunchAgents/com.user.sdlcontroller.plist

Now, a restart is enough, and the environment variable should always be set. You can verify it in Terminal using the command:

export

Now every SDL2 application should load and process this additional configuration. This helps me currently, as the controller is still recognized twice, but only one of the controllers responds directly when pressing buttons on Shadow PC.

Example of an Adjusted Mapping

Here is an example of a mapping string that you can adjust:

030000005e0400008e02000014010000,Xbox One Controller,a:b1,b:b2,x:b3,y:b4,back:b6,start:b7,leftstick:b8,rightstick:b9,leftshoulder:b4,rightshoulder:b5,dpup:h0.1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5

This string defines the button and axis assignments for an Xbox One controller. You can adjust the values to achieve the desired button mapping.

Another example is for disabling the duplicate Xbox 360 controller for a Xim Matrix in PC XInput mode on macOS 15 Sequoia:

030000005e0400008e02000014016800,Dead360,leftx:a6,lefty:a7,rightx:a8,righty:a9,platform:Mac OS X

Conclusion

With these steps, you can adjust the button mapping of your gamepad for the Shadow PC app and other SDL2 Apps and ensure that everything works as desired. It requires some experimentation, but by using SDL2, you have full control over the configuration of your gamepad.

Good luck and have fun gaming!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment