csutorasa / XOutput

DirectInput to XInput wrapper
MIT License
1.12k stars 97 forks source link

Feature request: Allow many-to-one control mappings #210

Open hifihedgehog opened 3 years ago

hifihedgehog commented 3 years ago

See above. We use XOutput a lot in the arcade cabinet scene. With XOutput, I can easily merge the two Ultimarc Ultrastik 360 joysticks and the pushbuttons from my USB button encoder on my arcade cabinet into two virtual Xinput controllers. It is so simple and it just works. I am finding that I would like many-to-one mapping since I sometime need to use an actual physical Xbox-style gamepad from time to time with some games I emulate like Breath of the Wild where a camera stick and the d-pad are necessary. Having to break out a keyboard and mouse to change the controller settings instead of staying within the flow and immersion of the arcade front-end interface (the current go-to is LaunchBox's BigBox, which I use) is the current drawback. But if I could have multiple possible inputs assigned to the outputs (one set of inputs from the arcade cabinet and the other set from the gamepads) so I could already have physical game controllers premapped or passed through to the virtual ones, I would not have to break that flow or immersion.

hifihedgehog commented 3 years ago

Here is some research I did.

Joystick Gremlin offers the functionality I speak of, but it only supports it for DirectInput. Joystick Gremelin allows any detected joystick, gamepad or keyboard's input (button or axe) to be mapped to the output (button or axe) of a virtual DirectInput controller.

Each button on a detected input device can be mapped to just a single output on the virtual controller, but there is no specified limit on the number of inputs from the input devices that can be mapped to each output on each virtual controller. Because of this many-to-one design philosophy for the controller mappings, there is no limit on the number of inputs that can be mapped to a button or axis on the output virtual controller. In an ideal world, a many-to-many design philosophy would be the ideal for the ultimate in customization, and one-to-many is already supported in XOutput, so I would love to see many-to-one customization in XOutput in the future.

For the time being, I will be running an input chain where (1) I first merge all my controllers in Joystick Gremlin to two DirectInput controllers, and (2) I second pass that DirectInput controller to XOutput which then converts those two DirectInput controllers into two XInput controllers. It is a bit messier than I would prefer and this multiplies the number of software failure points by two. That is why, for both simplicity and stability's sake, I would like to see the option to assign multiple inputs to each output in XOutput.

hifihedgehog commented 3 years ago

Long story short, someone created exactly what I was after here. I will be trying this out later today after some shut-eye:

fpdavis/MTOvJoyFeeder: Many To One Virtual Joystick Feeder. Maps one or more physical joysticks to one virtual joystick. (github.com)

I will close this ticket afterwards if it meets my requirements.

hifihedgehog commented 3 years ago

Come to find out, MTOvJoyFeeder is broken and abandoned. However, the code is there and, while I am not a C# coder, I am a coder and I can immediately see the techniques the dev used, which could be implemented here easily enough.

https://github.com/fpdavis/MTOvJoyFeeder/tree/master/

If you investigate the code, you could also add support for passing through Xinput controllers, a cool feature.

hifihedgehog commented 3 years ago

To be clear, when I say many to one, I mean Controller 1 Button A | (OR) Controller 2 Button A -> Virtual Controller Button A.

hifihedgehog commented 3 years ago

x360ce version 4 seems to have this functionality already. However, version 4 of x360ce has long, long list of bugs, such as not interfacing with HidGuardian correctly, not starting automatically with Windows when selected and not having a working Guide button. I will do my best to help contribute. I am reading the code right now.

hifihedgehog commented 3 years ago

By the way, @csutorasa, I just found a way to manually add the many-to-one mapping I told you about until you add it to XOutput's interface. For example, here is how it would work with a keyboard. I manually edited the settings.json like this to add two buttons (the letters "A" and "S") mapped to the B button on a virtual controller:

(...)
"B": {
          "Mappers": [
            {
              "InputDevice": "Keyboard",
              "InputType": "44",
              "MinValue": 0.0,
              "MaxValue": 1.0,
              "Deadzone": 0.0
            },
            {
              "InputDevice": "Keyboard",
              "InputType": "62",
              "MinValue": 0.0,
              "MaxValue": 1.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },

(...)

Your JSON fully supports tuples for inputs even though, at the same time, the front-end interface doesn't display them correctly (it only shows the first input of the tuple) or allow me to assign multiple inputs to an output via the interface. So many-to-one already works, functionally speaking.

hifihedgehog commented 3 years ago

Here is the full sample settings.json that shows that multiple inputs can be assigned to a controller output:

{
  "CloseToTray": false,
  "ShowAll": false,
  "HidGuardianEnabled": false,
  "Language": "English",
  "Input": {
    "Keyboard": {
      "ForceFeedback": false
    },
    "Mouse": {
      "ForceFeedback": false
    }
  },
  "Mapping": [
    {
      "StartWhenConnected": false,
      "Name": "Controller",
      "Id": "d5afc92a-e67e-4d51-97ca-aa84b418411d",
      "ForceFeedbackDevice": null,
      "Mappings": {
        "A": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "B": {
          "Mappers": [
            {
              "InputDevice": "Keyboard",
              "InputType": "44",
              "MinValue": 0.0,
              "MaxValue": 1.0,
              "Deadzone": 0.0
            },
            {
              "InputDevice": "Keyboard",
              "InputType": "62",
              "MinValue": 0.0,
              "MaxValue": 1.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "X": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "Y": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "L1": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "R1": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "L3": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "R3": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "Start": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "Back": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "Home": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "LX": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.5,
              "MaxValue": 0.5,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "LY": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.5,
              "MaxValue": 0.5,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "RX": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.5,
              "MaxValue": 0.5,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "RY": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.5,
              "MaxValue": 0.5,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "L2": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "R2": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "UP": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "DOWN": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "LEFT": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        },
        "RIGHT": {
          "Mappers": [
            {
              "InputDevice": null,
              "InputType": "0",
              "MinValue": 0.0,
              "MaxValue": 0.0,
              "Deadzone": 0.0
            }
          ],
          "CenterPoint": 0.0
        }
      }
    }
  ]
}