Per-device joystick mapping

Ask gaming related questions
Thraeg
Posts: 2
Joined: 30 Jan 2024, 16:44

Per-device joystick mapping

30 Jan 2024, 17:03

Hi. I'm trying to figure out if there's a way to have a script that only triggers off gamepad input from a specific device.

Specifically, this is for a Lenovo Legion Go, connected to an 8bitdo arcade stick, which can toggle between presenting itself to the system as an XBox 360 controller or a Switch Pro Controller.

I'd like to leave the Xbox mode alone to use in games, but map the buttons in Switch mode to keyboard commands for opening specific programs, volume control, opening the settings menu, etc.

I'd like to have it trigger off some consistent aspect of the Switch Pro Controller mode, so that it doesn't matter how many other game controllers are connected, or what order they were connected in -- Switch input gets translated to keyboard commands and other input passes through without being altered.

Is this possible?

Thanks
User avatar
Noitalommi_2
Posts: 286
Joined: 16 Aug 2023, 10:58

Re: Per-device joystick mapping

01 Feb 2024, 14:57

Hi @Thraeg

The problem will probably be that Autohotkey or Windows does not natively recognize input from the 8bitdo Arcade Stick in Switch mode
Because otherwise you wouldn't need a selector to switch between PC (Windows) and Switch (Console).
But alternatively you could do it so that you use the second selector (LS,DP,RS) to change the button layout because LS and D-pad are used in most games for directional input and RS is only used for the camera.

In short:

Selector is set to RS
move the Stick -> the Script detects input from RS -> activates the script

Selector is set to LS
move the Stick -> the Script detects input from LS -> deactivates the script

Here is an example which uses the XInput function to map a sound beep to the a-Button if RS is selected:

Code: Select all

#Requires AutoHotkey >=2.0
#SingleInstance


XInput_Init()

SetTimer Button, 100
Button() {

    Loop 4 {
        if State := XInput_GetState(A_Index-1)
            if State.wButtons & 0x1000 && on = true ; A-Button
                SoundBeep
    }
}
ToolTip "move the stick"
Loop {
    Loop 4 {
        if State := XInput_GetState(A_Index-1) {

            if State.sThumbLX || State.sThumbLY
                ToolTip("LS, A-Button Soundbeep disabled"), on := false
            else if State.sThumbRX || State.sThumbRY
                ToolTip("RS, A-Button Soundbeep enabled"), on := true
        }
    }
    Sleep 100
}

/*  XInput by Lexikos
 */

/*
    Function: XInput_Init

    Initializes XInput.ahk with the given XInput DLL.

    Parameters:
        dll     -   The path or name of the XInput DLL to load.
*/
XInput_Init(dll:="")
{
    global
    if _XInput_hm ?? 0
        return

    ;======== CONSTANTS DEFINED IN XINPUT.H ========

    ; Device types available in XINPUT_CAPABILITIES
    XINPUT_DEVTYPE_GAMEPAD          := 0x01

    ; Device subtypes available in XINPUT_CAPABILITIES
    XINPUT_DEVSUBTYPE_GAMEPAD       := 0x01

    ; Flags for XINPUT_CAPABILITIES
    XINPUT_CAPS_VOICE_SUPPORTED     := 0x0004

    ; Constants for gamepad buttons
    XINPUT_GAMEPAD_DPAD_UP          := 0x0001
    XINPUT_GAMEPAD_DPAD_DOWN        := 0x0002
    XINPUT_GAMEPAD_DPAD_LEFT        := 0x0004
    XINPUT_GAMEPAD_DPAD_RIGHT       := 0x0008
    XINPUT_GAMEPAD_START            := 0x0010
    XINPUT_GAMEPAD_BACK             := 0x0020
    XINPUT_GAMEPAD_LEFT_THUMB       := 0x0040
    XINPUT_GAMEPAD_RIGHT_THUMB      := 0x0080
    XINPUT_GAMEPAD_LEFT_SHOULDER    := 0x0100
    XINPUT_GAMEPAD_RIGHT_SHOULDER   := 0x0200
    XINPUT_GAMEPAD_GUIDE            := 0x0400
    XINPUT_GAMEPAD_A                := 0x1000
    XINPUT_GAMEPAD_B                := 0x2000
    XINPUT_GAMEPAD_X                := 0x4000
    XINPUT_GAMEPAD_Y                := 0x8000

    ; Gamepad thresholds
    XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE  := 7849
    XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE := 8689
    XINPUT_GAMEPAD_TRIGGER_THRESHOLD    := 30

    ; Flags to pass to XInputGetCapabilities
    XINPUT_FLAG_GAMEPAD             := 0x00000001

    ;=============== END CONSTANTS =================

    if (dll = "")
        Loop Files A_WinDir "\System32\XInput1_*.dll"
            dll := A_LoopFileName
    if (dll = "")
        dll := "XInput1_3.dll"

    _XInput_hm := DllCall("LoadLibrary" ,"str",dll ,"ptr")

    if !_XInput_hm
        throw Error("Failed to initialize XInput: " dll " not found.")

   (_XInput_GetState        := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"ptr",100 ,"ptr"))
|| (_XInput_GetState        := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"astr","XInputGetState" ,"ptr"))
    _XInput_SetState        := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"astr","XInputSetState" ,"ptr")
    _XInput_GetCapabilities := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"astr","XInputGetCapabilities" ,"ptr")

    if !(_XInput_GetState && _XInput_SetState && _XInput_GetCapabilities) {
        XInput_Term()
        throw Error("Failed to initialize XInput: function not found.")
    }
}

/*
    Function: XInput_GetState

    Retrieves the current state of the specified controller.

    Parameters:
        UserIndex   -   [in] Index of the user's controller. Can be a value from 0 to 3.

    Returns:
        An object with with properties equivalent to merging XINPUT_STATE and XINPUT_GAMEPAD.
        https://docs.microsoft.com/en-us/windows/win32/api/xinput/ns-xinput-xinput_state

    Error handling:
        Returns false if the controller is disconnected.
        Throws an OSError for other errors.
*/
XInput_GetState(UserIndex)
{
    global _XInput_GetState

    xiState := Buffer(16)

    if err := DllCall(_XInput_GetState ,"uint",UserIndex ,"ptr",xiState) {
        if err = 1167 ; ERROR_DEVICE_NOT_CONNECTED
            return 0
        throw OSError(err, -1)
    }

    return {
        dwPacketNumber: NumGet(xiState,  0, "UInt"),
        wButtons:       NumGet(xiState,  4, "UShort"),
        bLeftTrigger:   NumGet(xiState,  6, "UChar"),
        bRightTrigger:  NumGet(xiState,  7, "UChar"),
        sThumbLX:       NumGet(xiState,  8, "Short"),
        sThumbLY:       NumGet(xiState, 10, "Short"),
        sThumbRX:       NumGet(xiState, 12, "Short"),
        sThumbRY:       NumGet(xiState, 14, "Short"),
    }
}

/*
    Function: XInput_SetState

    Sends data to a connected controller. This function is used to activate the vibration
    function of a controller.

    Parameters:
        UserIndex       -   [in] Index of the user's controller. Can be a value from 0 to 3.
        LeftMotorSpeed  -   [in] Speed of the left motor, between 0 and 65535.
        RightMotorSpeed -   [in] Speed of the right motor, between 0 and 65535.

    Error handling:
        Throws an OSError on failure, such as if the controller is not connected.

    Remarks:
        The left motor is the low-frequency rumble motor. The right motor is the
        high-frequency rumble motor. The two motors are not the same, and they create
        different vibration effects.
*/
XInput_SetState(UserIndex, LeftMotorSpeed, RightMotorSpeed)
{
    global _XInput_SetState
    if err := DllCall(_XInput_SetState ,"uint",UserIndex ,"uint*",LeftMotorSpeed|RightMotorSpeed<<16)
        throw OSError(err, -1)
}

/*
    Function: XInput_GetCapabilities

    Retrieves the capabilities and features of a connected controller.

    Parameters:
        UserIndex   -   [in] Index of the user's controller. Can be a value in the range 0–3.
        Flags       -   [in] Input flags that identify the controller type.
                                0   - All controllers.
                                1   - XINPUT_FLAG_GAMEPAD: Xbox 360 Controllers only.

    Returns:
        An object with properties equivalent to XINPUT_CAPABILITIES.
        https://docs.microsoft.com/en-au/windows/win32/api/xinput/ns-xinput-xinput_capabilities

    Error handling:
        Returns false if the controller is disconnected.
        Throws an OSError for other errors.
*/
XInput_GetCapabilities(UserIndex, Flags)
{
    global _XInput_GetCapabilities

    xiCaps := Buffer(20)

    if err := DllCall(_XInput_GetCapabilities ,"uint",UserIndex ,"uint",Flags ,"ptr",xiCaps) {
        if err = 1167 ; ERROR_DEVICE_NOT_CONNECTED
            return 0
        throw OSError(err, -1)
    }

    return {
        Type:                   NumGet(xiCaps,  0, "UChar"),
        SubType:                NumGet(xiCaps,  1, "UChar"),
        Flags:                  NumGet(xiCaps,  2, "UShort"),
        Gamepad: {
            wButtons:           NumGet(xiCaps,  4, "UShort"),
            bLeftTrigger:       NumGet(xiCaps,  6, "UChar"),
            bRightTrigger:      NumGet(xiCaps,  7, "UChar"),
            sThumbLX:           NumGet(xiCaps,  8, "Short"),
            sThumbLY:           NumGet(xiCaps, 10, "Short"),
            sThumbRX:           NumGet(xiCaps, 12, "Short"),
            sThumbRY:           NumGet(xiCaps, 14, "Short")
        },
        Vibration: {
            wLeftMotorSpeed:    NumGet(xiCaps, 16, "UShort"),
            wRightMotorSpeed:   NumGet(xiCaps, 18, "UShort")
        }
    }
}

/*
    Function: XInput_Term
    Unloads the previously loaded XInput DLL.
*/
XInput_Term() {
    global
    if _XInput_hm
        DllCall("FreeLibrary","uint",_XInput_hm), _XInput_hm :=_XInput_GetState :=_XInput_SetState :=_XInput_GetCapabilities :=0
}

; TODO: XInputEnable, 'GetBatteryInformation and 'GetKeystroke.
or you could use a button combination like holding start & select for 2s to start / stop the script.

Code: Select all

#Requires AutoHotkey >=2.0
#SingleInstance


XInput_Init()

SetTimer Button, 100
Button() {

    Loop 4 {
        if State := XInput_GetState(A_Index-1)
            if State.wButtons & 0x1000 && on = true ; A-Button
                SoundBeep
    }
}

ToolTip "press and hold Start & Select for 2s"
on := false
Loop {
    Loop 4 {
        if State := XInput_GetState(Index := A_Index-1) {

            if State.wButtons = 0x0030 { ; Start & Select
                TickCount := A_TickCount
                while State.wButtons = 0x0030 {
                    State := XInput_GetState(Index)
                    Sleep 100
                    if A_TickCount-TickCount>=2000 {
                        on := !on
                        SoundBeep 700
                        ToolTip "A-Button sound beep " (on ? "on" : "off")
                        break
                    }
                }
            }
        }
    }
    Sleep 100
}

/*  XInput by Lexikos
 */

/*
    Function: XInput_Init

    Initializes XInput.ahk with the given XInput DLL.

    Parameters:
        dll     -   The path or name of the XInput DLL to load.
*/
XInput_Init(dll:="")
{
    global
    if _XInput_hm ?? 0
        return

    ;======== CONSTANTS DEFINED IN XINPUT.H ========

    ; Device types available in XINPUT_CAPABILITIES
    XINPUT_DEVTYPE_GAMEPAD          := 0x01

    ; Device subtypes available in XINPUT_CAPABILITIES
    XINPUT_DEVSUBTYPE_GAMEPAD       := 0x01

    ; Flags for XINPUT_CAPABILITIES
    XINPUT_CAPS_VOICE_SUPPORTED     := 0x0004

    ; Constants for gamepad buttons
    XINPUT_GAMEPAD_DPAD_UP          := 0x0001
    XINPUT_GAMEPAD_DPAD_DOWN        := 0x0002
    XINPUT_GAMEPAD_DPAD_LEFT        := 0x0004
    XINPUT_GAMEPAD_DPAD_RIGHT       := 0x0008
    XINPUT_GAMEPAD_START            := 0x0010
    XINPUT_GAMEPAD_BACK             := 0x0020
    XINPUT_GAMEPAD_LEFT_THUMB       := 0x0040
    XINPUT_GAMEPAD_RIGHT_THUMB      := 0x0080
    XINPUT_GAMEPAD_LEFT_SHOULDER    := 0x0100
    XINPUT_GAMEPAD_RIGHT_SHOULDER   := 0x0200
    XINPUT_GAMEPAD_GUIDE            := 0x0400
    XINPUT_GAMEPAD_A                := 0x1000
    XINPUT_GAMEPAD_B                := 0x2000
    XINPUT_GAMEPAD_X                := 0x4000
    XINPUT_GAMEPAD_Y                := 0x8000

    ; Gamepad thresholds
    XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE  := 7849
    XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE := 8689
    XINPUT_GAMEPAD_TRIGGER_THRESHOLD    := 30

    ; Flags to pass to XInputGetCapabilities
    XINPUT_FLAG_GAMEPAD             := 0x00000001

    ;=============== END CONSTANTS =================

    if (dll = "")
        Loop Files A_WinDir "\System32\XInput1_*.dll"
            dll := A_LoopFileName
    if (dll = "")
        dll := "XInput1_3.dll"

    _XInput_hm := DllCall("LoadLibrary" ,"str",dll ,"ptr")

    if !_XInput_hm
        throw Error("Failed to initialize XInput: " dll " not found.")

   (_XInput_GetState        := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"ptr",100 ,"ptr"))
|| (_XInput_GetState        := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"astr","XInputGetState" ,"ptr"))
    _XInput_SetState        := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"astr","XInputSetState" ,"ptr")
    _XInput_GetCapabilities := DllCall("GetProcAddress" ,"ptr",_XInput_hm ,"astr","XInputGetCapabilities" ,"ptr")

    if !(_XInput_GetState && _XInput_SetState && _XInput_GetCapabilities) {
        XInput_Term()
        throw Error("Failed to initialize XInput: function not found.")
    }
}

/*
    Function: XInput_GetState

    Retrieves the current state of the specified controller.

    Parameters:
        UserIndex   -   [in] Index of the user's controller. Can be a value from 0 to 3.

    Returns:
        An object with with properties equivalent to merging XINPUT_STATE and XINPUT_GAMEPAD.
        https://docs.microsoft.com/en-us/windows/win32/api/xinput/ns-xinput-xinput_state

    Error handling:
        Returns false if the controller is disconnected.
        Throws an OSError for other errors.
*/
XInput_GetState(UserIndex)
{
    global _XInput_GetState

    xiState := Buffer(16)

    if err := DllCall(_XInput_GetState ,"uint",UserIndex ,"ptr",xiState) {
        if err = 1167 ; ERROR_DEVICE_NOT_CONNECTED
            return 0
        throw OSError(err, -1)
    }

    return {
        dwPacketNumber: NumGet(xiState,  0, "UInt"),
        wButtons:       NumGet(xiState,  4, "UShort"),
        bLeftTrigger:   NumGet(xiState,  6, "UChar"),
        bRightTrigger:  NumGet(xiState,  7, "UChar"),
        sThumbLX:       NumGet(xiState,  8, "Short"),
        sThumbLY:       NumGet(xiState, 10, "Short"),
        sThumbRX:       NumGet(xiState, 12, "Short"),
        sThumbRY:       NumGet(xiState, 14, "Short"),
    }
}

/*
    Function: XInput_SetState

    Sends data to a connected controller. This function is used to activate the vibration
    function of a controller.

    Parameters:
        UserIndex       -   [in] Index of the user's controller. Can be a value from 0 to 3.
        LeftMotorSpeed  -   [in] Speed of the left motor, between 0 and 65535.
        RightMotorSpeed -   [in] Speed of the right motor, between 0 and 65535.

    Error handling:
        Throws an OSError on failure, such as if the controller is not connected.

    Remarks:
        The left motor is the low-frequency rumble motor. The right motor is the
        high-frequency rumble motor. The two motors are not the same, and they create
        different vibration effects.
*/
XInput_SetState(UserIndex, LeftMotorSpeed, RightMotorSpeed)
{
    global _XInput_SetState
    if err := DllCall(_XInput_SetState ,"uint",UserIndex ,"uint*",LeftMotorSpeed|RightMotorSpeed<<16)
        throw OSError(err, -1)
}

/*
    Function: XInput_GetCapabilities

    Retrieves the capabilities and features of a connected controller.

    Parameters:
        UserIndex   -   [in] Index of the user's controller. Can be a value in the range 0–3.
        Flags       -   [in] Input flags that identify the controller type.
                                0   - All controllers.
                                1   - XINPUT_FLAG_GAMEPAD: Xbox 360 Controllers only.

    Returns:
        An object with properties equivalent to XINPUT_CAPABILITIES.
        https://docs.microsoft.com/en-au/windows/win32/api/xinput/ns-xinput-xinput_capabilities

    Error handling:
        Returns false if the controller is disconnected.
        Throws an OSError for other errors.
*/
XInput_GetCapabilities(UserIndex, Flags)
{
    global _XInput_GetCapabilities

    xiCaps := Buffer(20)

    if err := DllCall(_XInput_GetCapabilities ,"uint",UserIndex ,"uint",Flags ,"ptr",xiCaps) {
        if err = 1167 ; ERROR_DEVICE_NOT_CONNECTED
            return 0
        throw OSError(err, -1)
    }

    return {
        Type:                   NumGet(xiCaps,  0, "UChar"),
        SubType:                NumGet(xiCaps,  1, "UChar"),
        Flags:                  NumGet(xiCaps,  2, "UShort"),
        Gamepad: {
            wButtons:           NumGet(xiCaps,  4, "UShort"),
            bLeftTrigger:       NumGet(xiCaps,  6, "UChar"),
            bRightTrigger:      NumGet(xiCaps,  7, "UChar"),
            sThumbLX:           NumGet(xiCaps,  8, "Short"),
            sThumbLY:           NumGet(xiCaps, 10, "Short"),
            sThumbRX:           NumGet(xiCaps, 12, "Short"),
            sThumbRY:           NumGet(xiCaps, 14, "Short")
        },
        Vibration: {
            wLeftMotorSpeed:    NumGet(xiCaps, 16, "UShort"),
            wRightMotorSpeed:   NumGet(xiCaps, 18, "UShort")
        }
    }
}

/*
    Function: XInput_Term
    Unloads the previously loaded XInput DLL.
*/
XInput_Term() {
    global
    if _XInput_hm
        DllCall("FreeLibrary","uint",_XInput_hm), _XInput_hm :=_XInput_GetState :=_XInput_SetState :=_XInput_GetCapabilities :=0
}

; TODO: XInputEnable, 'GetBatteryInformation and 'GetKeystroke.
Thraeg
Posts: 2
Joined: 30 Jan 2024, 16:44

Re: Per-device joystick mapping

01 Feb 2024, 21:18

Thanks! I'll try that out.

Return to “Gaming”

Who is online

Users browsing this forum: No registered users and 9 guests