The inotify limits are per user, so another program can exhaust them and make InotifyInit1 or InotifyAddWatch fail. That aborted RunGame with "gamepad: InotifyAddWatch failed: no space left on device" even though inotify is only used to detect hotplugged gamepads. GLFW ignores these errors, so do the same and keep the gamepads found by the initial scan. Closes #3304
734 lines
21 KiB
Go
734 lines
21 KiB
Go
// Copyright 2022 The Ebiten Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build !android && !nintendosdk && !playstation5
|
|
|
|
package gamepad
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2/internal/gamepaddb"
|
|
"github.com/hajimehoshi/ebiten/v2/internal/mathutil"
|
|
)
|
|
|
|
const dirName = "/dev/input"
|
|
|
|
var reEvent = regexp.MustCompile(`^event[0-9]+$`)
|
|
|
|
func isBitSet(s []byte, bit int) bool {
|
|
return s[bit/8]&(1<<(bit%8)) != 0
|
|
}
|
|
|
|
type nativeGamepadsImpl struct {
|
|
inotifyPlus1 int
|
|
watch int
|
|
}
|
|
|
|
func newNativeGamepadsImpl() nativeGamepads {
|
|
return &nativeGamepadsImpl{}
|
|
}
|
|
|
|
func (g *nativeGamepadsImpl) init(gamepads *gamepads) (err error) {
|
|
// Check the existence of the directory `dirName`.
|
|
var stat unix.Stat_t
|
|
if err := unix.Stat(dirName, &stat); err != nil {
|
|
if err == unix.ENOENT {
|
|
return nil
|
|
}
|
|
// `/dev/input` might not be accessible in some environments (#3057).
|
|
if err == unix.EACCES {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("gamepad: Stat failed: %w", err)
|
|
}
|
|
if stat.Mode&unix.S_IFDIR == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Another program of the same user can exhaust the inotify limits (#3304). That only costs
|
|
// hotplug detection, so it is not fatal here, and GLFW ignores the same errors.
|
|
if inotify, err := unix.InotifyInit1(unix.IN_NONBLOCK | unix.IN_CLOEXEC); err == nil {
|
|
g.inotifyPlus1 = inotify + 1
|
|
|
|
// Register for IN_ATTRIB to get notified when udev is done.
|
|
// This works well in practice but the true way is libudev.
|
|
if watch, err := unix.InotifyAddWatch(g.inotifyPlus1-1, dirName, unix.IN_CREATE|unix.IN_ATTRIB|unix.IN_DELETE); err == nil {
|
|
g.watch = watch
|
|
} else {
|
|
_ = unix.Close(g.inotifyPlus1 - 1)
|
|
g.inotifyPlus1 = 0
|
|
}
|
|
}
|
|
defer func() {
|
|
if err != nil && g.inotifyPlus1 != 0 {
|
|
_ = unix.Close(g.inotifyPlus1 - 1)
|
|
g.inotifyPlus1 = 0
|
|
}
|
|
}()
|
|
|
|
ents, err := os.ReadDir(dirName)
|
|
if err != nil {
|
|
return fmt.Errorf("gamepad: ReadDir(%s) failed: %w", dirName, err)
|
|
}
|
|
for _, ent := range ents {
|
|
if ent.IsDir() {
|
|
continue
|
|
}
|
|
if !reEvent.MatchString(ent.Name()) {
|
|
continue
|
|
}
|
|
if err := g.openGamepad(gamepads, filepath.Join(dirName, ent.Name())); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (*nativeGamepadsImpl) openGamepad(gamepads *gamepads, path string) error {
|
|
if gamepads.find(func(gamepad *Gamepad) bool {
|
|
return gamepad.native.(*nativeGamepadImpl).path == path
|
|
}) != nil {
|
|
return nil
|
|
}
|
|
|
|
// Rumble requires write access to upload and play force feedback effects.
|
|
// Fall back to read-only when write access is not permitted: the gamepad
|
|
// still works, without rumble.
|
|
writable := true
|
|
fd, err := unix.Open(path, unix.O_RDWR|unix.O_NONBLOCK, 0)
|
|
if err == unix.EACCES || err == unix.EPERM {
|
|
writable = false
|
|
fd, err = unix.Open(path, unix.O_RDONLY|unix.O_NONBLOCK, 0)
|
|
}
|
|
if err != nil {
|
|
if err == unix.EACCES {
|
|
return nil
|
|
}
|
|
// This happens with the Snap sandbox.
|
|
if err == unix.EPERM {
|
|
return nil
|
|
}
|
|
// This happens just after a disconnection.
|
|
if err == unix.ENOENT {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("gamepad: Open failed: %w", err)
|
|
}
|
|
owned := true
|
|
defer func() {
|
|
if owned {
|
|
_ = unix.Close(fd)
|
|
}
|
|
}()
|
|
|
|
evBits := make([]byte, (unix.EV_CNT+7)/8)
|
|
keyBits := make([]byte, (_KEY_CNT+7)/8)
|
|
absBits := make([]byte, (_ABS_CNT+7)/8)
|
|
var id input_id
|
|
if err := ioctl(fd, _EVIOCGBIT(0, uint(len(evBits))), unsafe.Pointer(&evBits[0])); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for evBits failed: %w", err)
|
|
}
|
|
if err := ioctl(fd, _EVIOCGBIT(unix.EV_KEY, uint(len(keyBits))), unsafe.Pointer(&keyBits[0])); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for keyBits failed: %w", err)
|
|
}
|
|
if err := ioctl(fd, _EVIOCGBIT(unix.EV_ABS, uint(len(absBits))), unsafe.Pointer(&absBits[0])); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for absBits failed: %w", err)
|
|
}
|
|
if err := ioctl(fd, _EVIOCGID(), unsafe.Pointer(&id)); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for an ID failed: %w", err)
|
|
}
|
|
|
|
if !isBitSet(evBits, unix.EV_ABS) {
|
|
owned = false
|
|
return unix.Close(fd)
|
|
}
|
|
|
|
cname := make([]byte, 256)
|
|
name := "Unknown"
|
|
// TODO: Is it OK to ignore the error here?
|
|
if err := ioctl(fd, uint(_EVIOCGNAME(uint(len(cname)))), unsafe.Pointer(&cname[0])); err == nil {
|
|
name = unix.ByteSliceToString(cname)
|
|
}
|
|
|
|
var sdlID string
|
|
if id.vendor != 0 && id.product != 0 && id.version != 0 {
|
|
sdlID = fmt.Sprintf("%02x%02x0000%02x%02x0000%02x%02x0000%02x%02x0000",
|
|
byte(id.bustype), byte(id.bustype>>8),
|
|
byte(id.vendor), byte(id.vendor>>8),
|
|
byte(id.product), byte(id.product>>8),
|
|
byte(id.version), byte(id.version>>8))
|
|
} else {
|
|
bs := []byte(name)
|
|
if len(bs) < 12 {
|
|
bs = append(bs, make([]byte, 12-len(bs))...)
|
|
}
|
|
sdlID = fmt.Sprintf("%02x%02x0000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
|
byte(id.bustype), byte(id.bustype>>8),
|
|
bs[0], bs[1], bs[2], bs[3], bs[4], bs[5], bs[6], bs[7], bs[8], bs[9], bs[10], bs[11])
|
|
}
|
|
|
|
supportsRumble := false
|
|
if writable && isBitSet(evBits, unix.EV_FF) {
|
|
ffBits := make([]byte, (_FF_CNT+7)/8)
|
|
if err := ioctl(fd, _EVIOCGBIT(unix.EV_FF, uint(len(ffBits))), unsafe.Pointer(&ffBits[0])); err == nil {
|
|
supportsRumble = isBitSet(ffBits, _FF_RUMBLE)
|
|
}
|
|
}
|
|
|
|
n := &nativeGamepadImpl{
|
|
path: path,
|
|
fdPlus1: fd + 1,
|
|
supportsRumble: supportsRumble,
|
|
effectID: -1,
|
|
}
|
|
|
|
var axisCount int
|
|
var buttonCount int
|
|
var hatCount int
|
|
for i := range n.keyMap {
|
|
n.keyMap[i] = -1
|
|
}
|
|
for i := range n.absMap {
|
|
n.absMap[i] = -1
|
|
}
|
|
for code := _BTN_MISC; code < _KEY_CNT; code++ {
|
|
if !isBitSet(keyBits, code) {
|
|
continue
|
|
}
|
|
n.keyMap[code-_BTN_MISC] = buttonCount
|
|
buttonCount++
|
|
}
|
|
for code := 0; code < _ABS_CNT; code++ {
|
|
if !isBitSet(absBits, code) {
|
|
continue
|
|
}
|
|
if code >= _ABS_HAT0X && code <= _ABS_HAT3Y {
|
|
// Write the hat index both for the X and the Y hat axis.
|
|
// That way, the hat can be referenced using either axis, which is used by the code building hatMappingInput.
|
|
n.absMap[code] = hatCount
|
|
code++
|
|
n.absMap[code] = hatCount
|
|
hatCount++
|
|
continue
|
|
}
|
|
if err := ioctl(n.fdPlus1-1, uint(_EVIOCGABS(uint(code))), unsafe.Pointer(&n.absInfo[code])); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for an abs at openGamepad failed: %w", err)
|
|
}
|
|
n.absMap[code] = axisCount
|
|
axisCount++
|
|
}
|
|
|
|
n.axisCount_ = axisCount
|
|
n.buttonCount_ = buttonCount
|
|
n.hatCount_ = hatCount
|
|
|
|
n.computeStandardLayout(id.vendor)
|
|
|
|
if err := n.pollAbsState(); err != nil {
|
|
return err
|
|
}
|
|
|
|
owned = false
|
|
gp := gamepads.add(name, sdlID)
|
|
gp.native = n
|
|
runtime.AddCleanup(gp, func(n *nativeGamepadImpl) {
|
|
n.close()
|
|
}, n)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *nativeGamepadsImpl) update(gamepads *gamepads) error {
|
|
if g.inotifyPlus1 == 0 {
|
|
return nil
|
|
}
|
|
|
|
buf := make([]byte, 16384)
|
|
n, err := unix.Read(g.inotifyPlus1-1, buf[:])
|
|
if err != nil {
|
|
if err == unix.EAGAIN {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("gamepad: Read failed: %w", err)
|
|
}
|
|
buf = buf[:n]
|
|
|
|
for len(buf) > 0 {
|
|
e := unix.InotifyEvent{
|
|
Wd: int32(buf[0]) | int32(buf[1])<<8 | int32(buf[2])<<16 | int32(buf[3])<<24,
|
|
Mask: uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16 | uint32(buf[7])<<24,
|
|
Cookie: uint32(buf[8]) | uint32(buf[9])<<8 | uint32(buf[10])<<16 | uint32(buf[11])<<24,
|
|
Len: uint32(buf[12]) | uint32(buf[13])<<8 | uint32(buf[14])<<16 | uint32(buf[15])<<24,
|
|
}
|
|
if e.Len == 0 {
|
|
buf = buf[16:]
|
|
continue
|
|
}
|
|
name := unix.ByteSliceToString(buf[16 : 16+e.Len-1]) // len includes the null terminate.
|
|
buf = buf[16+e.Len:]
|
|
if !reEvent.MatchString(name) {
|
|
continue
|
|
}
|
|
|
|
path := filepath.Join(dirName, name)
|
|
if e.Mask&(unix.IN_CREATE|unix.IN_ATTRIB) != 0 {
|
|
if err := g.openGamepad(gamepads, path); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if e.Mask&unix.IN_DELETE != 0 {
|
|
if gp := gamepads.find(func(gamepad *Gamepad) bool {
|
|
return gamepad.native.(*nativeGamepadImpl).path == path
|
|
}); gp != nil {
|
|
// Lock the gamepad so the close cannot race with a
|
|
// concurrent Vibrate using the file descriptor.
|
|
withNative(gp, func(n *nativeGamepadImpl) {
|
|
n.close()
|
|
})
|
|
gamepads.remove(func(gamepad *Gamepad) bool {
|
|
return gamepad == gp
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type nativeGamepadImpl struct {
|
|
fdPlus1 int
|
|
path string
|
|
keyMap [_KEY_CNT - _BTN_MISC]int
|
|
absMap [_ABS_CNT]int
|
|
absInfo [_ABS_CNT]input_absinfo
|
|
dropped bool
|
|
|
|
supportsRumble bool
|
|
effectID int16
|
|
|
|
axes [_ABS_CNT]float64
|
|
buttons [_KEY_CNT - _BTN_MISC]bool
|
|
hats [4]int
|
|
|
|
axisCount_ int
|
|
buttonCount_ int
|
|
hatCount_ int
|
|
|
|
stdAxisMap map[gamepaddb.StandardAxis]mappingInput
|
|
stdButtonMap map[gamepaddb.StandardButton]mappingInput
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) close() {
|
|
if g.fdPlus1 == 0 {
|
|
return
|
|
}
|
|
_ = unix.Close(g.fdPlus1 - 1)
|
|
g.fdPlus1 = 0
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) update(gamepad *gamepads) error {
|
|
if g.fdPlus1 == 0 {
|
|
return nil
|
|
}
|
|
|
|
for {
|
|
buf := make([]byte, unsafe.Sizeof(input_event{}))
|
|
// TODO: Should the returned byte count be cared?
|
|
if _, err := unix.Read(g.fdPlus1-1, buf); err != nil {
|
|
if err == unix.EAGAIN {
|
|
break
|
|
}
|
|
// Disconnected
|
|
if err == unix.ENODEV {
|
|
g.close()
|
|
return nil
|
|
}
|
|
return fmt.Errorf("gamepad: Read failed: %w", err)
|
|
}
|
|
|
|
const (
|
|
offsetTyp = unsafe.Offsetof(input_event{}.typ)
|
|
offsetCode = unsafe.Offsetof(input_event{}.code)
|
|
offsetValue = unsafe.Offsetof(input_event{}.value)
|
|
)
|
|
// time is not used.
|
|
e := input_event{
|
|
typ: uint16(buf[offsetTyp]) | uint16(buf[offsetTyp+1])<<8,
|
|
code: uint16(buf[offsetCode]) | uint16(buf[offsetCode+1])<<8,
|
|
value: int32(buf[offsetValue]) | int32(buf[offsetValue+1])<<8 | int32(buf[offsetValue+2])<<16 | int32(buf[offsetValue+3])<<24,
|
|
}
|
|
|
|
if e.typ == unix.EV_SYN && e.code == _SYN_DROPPED {
|
|
g.dropped = true
|
|
}
|
|
if g.dropped {
|
|
// Ignore events through the next SYN_REPORT, then restore the device state.
|
|
if e.typ == unix.EV_SYN && e.code == _SYN_REPORT {
|
|
if err := g.pollAbsState(); err != nil {
|
|
return fmt.Errorf("gamepad: poll absolute state: %w", err)
|
|
}
|
|
if err := g.pollKeyState(); err != nil {
|
|
return fmt.Errorf("gamepad: poll key state: %w", err)
|
|
}
|
|
g.dropped = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch e.typ {
|
|
case unix.EV_KEY:
|
|
if int(e.code-_BTN_MISC) < len(g.keyMap) {
|
|
idx := g.keyMap[e.code-_BTN_MISC]
|
|
if idx < 0 {
|
|
continue
|
|
}
|
|
g.buttons[idx] = e.value != 0
|
|
}
|
|
case unix.EV_ABS:
|
|
g.handleAbsEvent(int(e.code), e.value)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) pollKeyState() error {
|
|
var keyBits [(_KEY_CNT + 7) / 8]byte
|
|
if err := ioctl(g.fdPlus1-1, _EVIOCGKEY(uint(len(keyBits))), unsafe.Pointer(&keyBits[0])); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for keys at pollKeyState failed: %w", err)
|
|
}
|
|
for code, index := range g.keyMap {
|
|
if index >= 0 {
|
|
g.buttons[index] = isBitSet(keyBits[:], code+_BTN_MISC)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) pollAbsState() error {
|
|
for code := range _ABS_CNT {
|
|
if g.absMap[code] < 0 {
|
|
continue
|
|
}
|
|
if err := ioctl(g.fdPlus1-1, uint(_EVIOCGABS(uint(code))), unsafe.Pointer(&g.absInfo[code])); err != nil {
|
|
return fmt.Errorf("gamepad: ioctl for an abs at pollAbsState failed: %w", err)
|
|
}
|
|
g.handleAbsEvent(code, g.absInfo[code].value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) handleAbsEvent(code int, value int32) {
|
|
index := g.absMap[code]
|
|
if index < 0 {
|
|
return
|
|
}
|
|
|
|
if code >= _ABS_HAT0X && code <= _ABS_HAT3Y {
|
|
axis := (code - _ABS_HAT0X) % 2
|
|
|
|
switch axis {
|
|
case 0:
|
|
switch {
|
|
case value < 0:
|
|
g.hats[index] |= hatLeft
|
|
g.hats[index] &^= hatRight
|
|
case value > 0:
|
|
g.hats[index] &^= hatLeft
|
|
g.hats[index] |= hatRight
|
|
default:
|
|
g.hats[index] &^= hatLeft | hatRight
|
|
}
|
|
case 1:
|
|
switch {
|
|
case value < 0:
|
|
g.hats[index] |= hatUp
|
|
g.hats[index] &^= hatDown
|
|
case value > 0:
|
|
g.hats[index] &^= hatUp
|
|
g.hats[index] |= hatDown
|
|
default:
|
|
g.hats[index] &^= hatUp | hatDown
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
info := g.absInfo[code]
|
|
v := float64(value)
|
|
if r := float64(info.maximum) - float64(info.minimum); r != 0 {
|
|
v = (v - float64(info.minimum)) / r
|
|
v = v*2 - 1
|
|
}
|
|
g.axes[index] = v
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) computeStandardLayout(vendor uint16) {
|
|
g.stdAxisMap = map[gamepaddb.StandardAxis]mappingInput{}
|
|
g.stdButtonMap = map[gamepaddb.StandardButton]mappingInput{}
|
|
|
|
// NOTE: assignments to the same value are in exact reverse order as SDL2,
|
|
// so we can just overwrite rather than checking.
|
|
|
|
// BTN_GAMEPAD implies that the kernel module implements standard mapping.
|
|
if b := g.keyMap[_BTN_GAMEPAD-_BTN_MISC]; b < 0 {
|
|
return
|
|
}
|
|
|
|
// A and B buttons go by name.
|
|
if b := g.keyMap[_BTN_A-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightBottom] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_B-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightRight] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if vendor == 0x054c /* USB_VENDOR_SONY */ {
|
|
// Sony uses WEST/NORTH buttons.
|
|
if b := g.keyMap[_BTN_WEST-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightLeft] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_NORTH-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightTop] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
} else {
|
|
// Xbox uses X/Y buttons.
|
|
// Note that this is the opposite assignment following the WEST/NORTH mappings,
|
|
// and contradicts Linux kernel documentation which states
|
|
// that buttons are always assigned by physical location.
|
|
// However, it matches actual Xbox gamepads, and SDL2 has the same logic.
|
|
if b := g.keyMap[_BTN_X-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightLeft] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_Y-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightTop] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
}
|
|
|
|
// Center and thumb buttons.
|
|
if b := g.keyMap[_BTN_SELECT-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonCenterLeft] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_START-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonCenterRight] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_THUMBL-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftStick] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_THUMBR-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonRightStick] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_MODE-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonCenterCenter] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
|
|
// Shoulder buttons can be analog or digital. Prefer digital ones.
|
|
if h := g.absMap[_ABS_HAT1Y]; h >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontTopLeft] = hatMappingInput{g: g, hat: h, direction: hatDown}
|
|
}
|
|
if h := g.absMap[_ABS_HAT1X]; h >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontTopRight] = hatMappingInput{g: g, hat: h, direction: hatRight}
|
|
}
|
|
if b := g.keyMap[_BTN_TL-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontTopLeft] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_TR-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontTopRight] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
|
|
// Triggers can be analog or digital. Prefer analog ones.
|
|
if b := g.keyMap[_BTN_TL2-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontBottomLeft] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_TR2-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontBottomRight] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if a := g.absMap[_ABS_Z]; a >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontBottomLeft] = axisMappingInput{g: g, axis: a}
|
|
}
|
|
if a := g.absMap[_ABS_RZ]; a >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontBottomRight] = axisMappingInput{g: g, axis: a}
|
|
}
|
|
if h := g.absMap[_ABS_HAT2Y]; h >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontBottomLeft] = hatMappingInput{g: g, hat: h, direction: hatDown}
|
|
}
|
|
if h := g.absMap[_ABS_HAT2X]; h >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonFrontBottomRight] = hatMappingInput{g: g, hat: h, direction: hatRight}
|
|
}
|
|
|
|
// D-pad can be analog or digital. Prefer digital one.
|
|
if h := g.absMap[_ABS_HAT0X]; h >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftLeft] = hatMappingInput{g: g, hat: h, direction: hatLeft}
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftRight] = hatMappingInput{g: g, hat: h, direction: hatRight}
|
|
}
|
|
if h := g.absMap[_ABS_HAT0Y]; h >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftTop] = hatMappingInput{g: g, hat: h, direction: hatUp}
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftBottom] = hatMappingInput{g: g, hat: h, direction: hatDown}
|
|
}
|
|
if b := g.keyMap[_BTN_DPAD_UP-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftTop] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_DPAD_DOWN-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftBottom] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_DPAD_LEFT-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftLeft] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
if b := g.keyMap[_BTN_DPAD_RIGHT-_BTN_MISC]; b >= 0 {
|
|
g.stdButtonMap[gamepaddb.StandardButtonLeftRight] = buttonMappingInput{g: g, button: b}
|
|
}
|
|
|
|
// Left stick.
|
|
if a := g.absMap[_ABS_X]; a >= 0 {
|
|
g.stdAxisMap[gamepaddb.StandardAxisLeftStickHorizontal] = axisMappingInput{g: g, axis: a}
|
|
}
|
|
if a := g.absMap[_ABS_Y]; a >= 0 {
|
|
g.stdAxisMap[gamepaddb.StandardAxisLeftStickVertical] = axisMappingInput{g: g, axis: a}
|
|
}
|
|
|
|
// Right stick.
|
|
if a := g.absMap[_ABS_RX]; a >= 0 {
|
|
g.stdAxisMap[gamepaddb.StandardAxisRightStickHorizontal] = axisMappingInput{g: g, axis: a}
|
|
}
|
|
if a := g.absMap[_ABS_RY]; a >= 0 {
|
|
g.stdAxisMap[gamepaddb.StandardAxisRightStickVertical] = axisMappingInput{g: g, axis: a}
|
|
}
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) hasOwnStandardLayoutMapping() bool {
|
|
return len(g.stdAxisMap) != 0 || len(g.stdButtonMap) != 0
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) standardAxisInOwnMapping(axis gamepaddb.StandardAxis) mappingInput {
|
|
return g.stdAxisMap[axis]
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) standardButtonInOwnMapping(button gamepaddb.StandardButton) mappingInput {
|
|
return g.stdButtonMap[button]
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) axisCount() int {
|
|
return g.axisCount_
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) buttonCount() int {
|
|
return g.buttonCount_
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) hatCount() int {
|
|
return g.hatCount_
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
|
|
return axis >= 0 && axis < g.axisCount()
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
|
|
if axis < 0 || axis >= g.axisCount_ {
|
|
return 0
|
|
}
|
|
return g.axes[axis]
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) isButtonPressed(button int) bool {
|
|
if button < 0 || button >= g.buttonCount_ {
|
|
return false
|
|
}
|
|
return g.buttons[button]
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) buttonValue(button int) float64 {
|
|
if g.isButtonPressed(button) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) hatState(hat int) int {
|
|
if hat < 0 || hat >= g.hatCount_ {
|
|
return hatCentered
|
|
}
|
|
return g.hats[hat]
|
|
}
|
|
|
|
func (g *nativeGamepadImpl) vibrate(duration time.Duration, strongMagnitude float64, weakMagnitude float64) {
|
|
if !g.supportsRumble || g.fdPlus1 == 0 {
|
|
return
|
|
}
|
|
|
|
strongMagnitude = mathutil.Clamp01(strongMagnitude)
|
|
weakMagnitude = mathutil.Clamp01(weakMagnitude)
|
|
|
|
if strongMagnitude <= 0 && weakMagnitude <= 0 {
|
|
g.writeFFEvent(0)
|
|
return
|
|
}
|
|
|
|
if duration <= 0 {
|
|
return
|
|
}
|
|
|
|
// The kernel stops the effect once the replay length has passed, so no
|
|
// duration tracking is needed here. A replay length of 0 would play the
|
|
// effect with no time limit, so keep it at least 1.
|
|
ms := duration.Milliseconds()
|
|
if ms < 1 {
|
|
ms = 1
|
|
}
|
|
if ms > 0xffff {
|
|
ms = 0xffff
|
|
}
|
|
|
|
// An ID of -1 lets the kernel assign an ID to a new effect. Uploading
|
|
// with the ID of an already uploaded effect updates the effect in place.
|
|
effect := ff_effect{
|
|
typ: _FF_RUMBLE,
|
|
id: g.effectID,
|
|
}
|
|
effect.replay.length = uint16(ms)
|
|
effect.u.rumble.strong_magnitude = motorMagnitude(strongMagnitude)
|
|
effect.u.rumble.weak_magnitude = motorMagnitude(weakMagnitude)
|
|
|
|
if err := ioctl(g.fdPlus1-1, _EVIOCSFF(), unsafe.Pointer(&effect)); err != nil {
|
|
return
|
|
}
|
|
g.effectID = effect.id
|
|
|
|
g.writeFFEvent(1)
|
|
}
|
|
|
|
// writeFFEvent starts (value 1) or stops (value 0) playing the uploaded force
|
|
// feedback effect. It does nothing when no effect has been uploaded.
|
|
func (g *nativeGamepadImpl) writeFFEvent(value int32) {
|
|
if g.effectID < 0 {
|
|
return
|
|
}
|
|
e := input_event{
|
|
typ: unix.EV_FF,
|
|
code: uint16(g.effectID),
|
|
value: value,
|
|
}
|
|
_, _ = unix.Write(g.fdPlus1-1, unsafe.Slice((*byte)(unsafe.Pointer(&e)), int(unsafe.Sizeof(e))))
|
|
}
|