Getting Joystick/gamepad input inc. controller type

Hello Everyone
I’m currently using GitHub - 0xcafed00d/joystick: Go Joystick API to get joystick state - which (in basic form) works on Windows with XBox360 (compatible) controller, DualShock 4 and also Legion Go joystick/s.

However, the button mappings between DS4 and Xbox for the face buttons (and I am sure the axes) don’t map the same, i.e. I would like to have the same button (mask) for these:

  • Ⓐ/✕
  • Ⓑ/◯
  • Ⓧ/☐
  • Ⓨ/🛆

There doesn’t appear to be any way to identify the controller type (using the underlying Windows Multi Media (legacy) dll/api).

Does anyone know of a package that manages this better - without adopting a whole framework - note that this isn’t for game development - but for a background (OS level) utility that can respond to gamepad inputs.

Things I have looked at so far include:

  1. I can guess which the controllers are, since the DS4 returns a different number of buttons and axes to an XBox360 - but I can’t find anywhere this is documented so it is just guessing based on 2/3 controllers…
  2. Roll my own Windows calls - but this seems over the top (and OS specific) plus I don’t have any experience of this - and likely reinventing the wheel…
  3. Windows Gaming Input - but I don’t see any examples of Golang accessing the api and it looks like you might have to buy in to the whole SDK.
  4. SDL - but this looks too over the top for my purposes and it looks unlikely to solve the issue either

Note that I would prefer a cross platform solution but the prime target is windows…

Thank you in advance - Andy

I have tried some different options which may help anyone with a similar issue:

  1. I directly accessed the windows xinput dll. This worked for XBox 360 compatible joysticks (only). Unfortunately the joystick numbers do not match the Multi Media numbers so can’t be used to identify Xbox joysticks.

  2. I tried accessing the (new) GameInput and managed to get the dll and also access the first api call. But my Windows C++ is non existent so I gave up when I encountered pointers of pointers - too much learning required to convert between C++ and Go.

I’m now ‘rolling my own’ using XInput with XInputGetState from xinput1_3.dll.

I did revert to using GitHub - 0xcafed00d/joystick: Go Joystick API but gave up after using on a Legion Go and seeing very odd gamepad properties (I think directinput controller).

Best wishes - Andy

FYI, here is my solution in case it is of use to others …

The main call will be
go gamepad.CheckChanged()
this will send a socket broadcast json (in my case) to any listening clients…

The package is

package gamepad

import (
	"encoding/json"
	"fmt"
	"quando/internal/server/socket"
	"syscall"
	"time"
	"unsafe"
)

const (
	MAX_GAMEPADS        = 4
	XINPUT_DLL_FILENAME = "xinput1_3.dll"
	XINPUT_GET_STATE    = "XInputGetState"
	// Buton mapping for reference
	// "UP" 0x0001
	// "DOWN" 0x0002
	// "LEFT" 0x0004
	// "RIGHT" 0x0008
	// "START" 0x0010
	// "BACK" 0x0020
	// "L_STICK" 0x0040
	// "R_STICK" 0x0080
	// "L_BUMPER" 0x0100
	// "R_BUMPER" 0x0200
	// "A" 0x1000
	// "B" 0x2000
	// "X" 0x4000
	// "Y" 0x8000
	// N.B. HOME/GUIDE Does not map with standard call - would be 0x0400
)

var getState *syscall.Proc

type Gamepad struct {
	_             uint32 // packet - updates too often and not useful
	button_mask   uint16
	left_trigger  uint8
	right_trigger uint8
	left_x        int16
	left_y        int16
	right_x       int16
	right_y       int16
}

var gamepads [MAX_GAMEPADS]*Gamepad // stores the last returned to identify changes - or nil

type gamepadJSON struct {
	Id       int8   `json:"id"`
	Drop     bool   `json:"drop,omitempty"`
	Mask     uint16 `json:"mask,omitempty"`
	Ltrigger uint8  `json:"l_trigger,omitempty"`
	Rtrigger uint8  `json:"r_trigger,omitempty"`
	Lx       int16  `json:"l_x,omitempty"`
	Ly       int16  `json:"l_y,omitempty"`
	Rx       int16  `json:"r_x,omitempty"`
	Ry       int16  `json:"r_y,omitempty"`
}

func triggersChanged(gamepad_old, gamepad_new Gamepad) bool {
	// return as soon as we detect a change
	if gamepad_old.left_trigger != gamepad_new.left_trigger {
		return true
	}
	return gamepad_old.right_trigger != gamepad_new.right_trigger
}

func axesChanged(gamepad_old, gamepad_new Gamepad) bool {
	// return as soon as we detect a change
	if gamepad_old.left_x != gamepad_new.left_x {
		return true
	}
	if gamepad_old.left_y != gamepad_new.left_y {
		return true
	}
	if gamepad_old.right_x != gamepad_new.right_x {
		return true
	}
	return gamepad_old.right_y != gamepad_new.right_y
}

func gamepadUpdated(num uint) bool {
	changed := false
	var gamepad Gamepad
	result, _, _ := getState.Call(uintptr(num), uintptr(unsafe.Pointer(&gamepad)))
	if result == 0 { // success
		if gamepads[num] == nil {
			fmt.Println("Gamepad connected : ", num)
			changed = true
		} else {
			var last_gamepad = gamepads[num]
			if last_gamepad.button_mask != gamepad.button_mask {
				changed = true
			} else if triggersChanged(*last_gamepad, gamepad) {
				changed = true
			} else if axesChanged(*last_gamepad, gamepad) {
				changed = true
			}
		}
		gamepads[num] = &gamepad // always update even state hasn't changed
	} else if gamepads[num] != nil { // has just disconnected
		changed = true
		gamepads[num] = nil
	}
	return changed
}

func addPostJSON(gamepad *Gamepad, num int, gamepad_json *gamepadJSON) {
	gamepad_json.Id = int8(num)
	if gamepad == nil { // dropped
		gamepad_json.Drop = true
	} else {
		gamepad_json.Mask = gamepad.button_mask
		gamepad_json.Ltrigger = gamepad.left_trigger
		gamepad_json.Rtrigger = gamepad.right_trigger
		gamepad_json.Lx = gamepad.left_x
		gamepad_json.Ly = gamepad.left_y
		gamepad_json.Rx = gamepad.right_x
		gamepad_json.Ry = gamepad.right_y
	}
}

func CheckChanged() {
	if getState == nil {
		fmt.Println("** XInput joystick not being checked...")
		return
	} // else
	for {
		updated := false
		gamepad_json := gamepadJSON{}
		for num := range MAX_GAMEPADS { // note this will be 0..3
			if gamepadUpdated(uint(num)) {
				updated = true
				var gamepad *Gamepad // is nil
				if gamepads[num] != nil {
					gamepad = gamepads[num]
				}
				addPostJSON(gamepad, num, &gamepad_json)
			}
		}
		if updated {
			bout, err := json.Marshal(gamepad_json)
			if err != nil {
				fmt.Println("Error marshalling gamepad", err)
			} else {
				str := string(bout)
				prefix := `{"type":"gamepad"`
				if str != "{}" {
					prefix += ","
				}
				str = prefix + str[1:]
				socket.Broadcast(str)
			}
		}
		time.Sleep(time.Second / 60) // 60 times a second
	}
}

func init() {
	dll, err := syscall.LoadDLL(XINPUT_DLL_FILENAME) // use older version for now
	if err != nil {
		fmt.Println("** Failed to find", XINPUT_DLL_FILENAME)
	} else {
		getState, err = dll.FindProc(XINPUT_GET_STATE)
		if err != nil {
			fmt.Println("** Failed to find proc :", XINPUT_GET_STATE)
		}
	}
}