ebiten/internal/gamepaddb/gamepaddb.go
Hajime Hoshi 47558d20c5 internal/gamepaddb: enable the database for Android
Before this fix, the button and axis IDs are from the OS. These
didn't match with the SDL game controller databaes unfortunately.

This fix changes the assignments of the buttons and the axes to match
with the database.

Closes #2312
2022-09-09 22:20:39 +09:00

761 lines
19 KiB
Go

// Copyright 2021 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.
// gamecontrollerdb.txt is downloaded at https://github.com/gabomdq/SDL_GameControllerDB.
// To update the database file, run:
//
// curl --location --remote-name https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt
//go:generate go run github.com/hajimehoshi/file2byteslice/cmd/file2byteslice@v1.0.0 -package gamepaddb -input=./gamecontrollerdb.txt -output=./gamecontrollerdb.txt.go -var=gamecontrollerdbTxt
package gamepaddb
import (
"bufio"
"bytes"
"encoding/hex"
"fmt"
"runtime"
"strconv"
"strings"
"sync"
)
type platform int
const (
platformUnknown platform = iota
platformWindows
platformMacOS
platformUnix
platformAndroid
platformIOS
)
var currentPlatform platform
func init() {
if runtime.GOOS == "windows" {
currentPlatform = platformWindows
return
}
if runtime.GOOS == "aix" ||
runtime.GOOS == "dragonfly" ||
runtime.GOOS == "freebsd" ||
runtime.GOOS == "hurd" ||
runtime.GOOS == "illumos" ||
runtime.GOOS == "linux" ||
runtime.GOOS == "netbsd" ||
runtime.GOOS == "openbsd" ||
runtime.GOOS == "solaris" {
currentPlatform = platformUnix
return
}
if runtime.GOOS == "android" {
currentPlatform = platformAndroid
return
}
if isIOS {
currentPlatform = platformIOS
return
}
if runtime.GOOS == "darwin" {
currentPlatform = platformMacOS
return
}
}
var additionalGLFWGamepads = []byte(`
78696e70757401000000000000000000,XInput Gamepad (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757402000000000000000000,XInput Wheel (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757403000000000000000000,XInput Arcade Stick (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757404000000000000000000,XInput Flight Stick (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757405000000000000000000,XInput Dance Pad (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757406000000000000000000,XInput Guitar (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
78696e70757408000000000000000000,XInput Drum Kit (GLFW),platform:Windows,a:b0,b:b1,x:b2,y:b3,leftshoulder:b4,rightshoulder:b5,back:b6,start:b7,leftstick:b8,rightstick:b9,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,
`)
func init() {
if err := Update(gamecontrollerdbTxt); err != nil {
panic(err)
}
if err := Update(additionalGLFWGamepads); err != nil {
panic(err)
}
}
type mappingType int
const (
mappingTypeButton mappingType = iota
mappingTypeAxis
mappingTypeHat
)
const (
HatUp = 1
HatRight = 2
HatDown = 4
HatLeft = 8
)
type mapping struct {
Type mappingType
Index int
AxisScale int
AxisOffset int
HatState int
}
var (
gamepadNames = map[string]string{}
gamepadButtonMappings = map[string]map[StandardButton]*mapping{}
gamepadAxisMappings = map[string]map[StandardAxis]*mapping{}
mappingsM sync.RWMutex
)
func parseLine(line string, platform platform) (id string, name string, buttons map[StandardButton]*mapping, axes map[StandardAxis]*mapping, err error) {
line = strings.TrimSpace(line)
if len(line) == 0 {
return "", "", nil, nil, nil
}
if line[0] == '#' {
return "", "", nil, nil, nil
}
tokens := strings.Split(line, ",")
if len(tokens) < 2 {
return "", "", nil, nil, fmt.Errorf("gamepaddb: syntax error")
}
for _, token := range tokens[2:] {
if len(token) == 0 {
continue
}
tks := strings.Split(token, ":")
if len(tks) < 2 {
return "", "", nil, nil, fmt.Errorf("gamepaddb: syntax error")
}
// Note that the platform part is listed in the definition of SDL_GetPlatform.
if tks[0] == "platform" {
switch tks[1] {
case "Windows":
if platform != platformWindows {
return "", "", nil, nil, nil
}
case "Mac OS X":
if platform != platformMacOS {
return "", "", nil, nil, nil
}
case "Linux":
if platform != platformUnix {
return "", "", nil, nil, nil
}
case "Android":
if platform != platformAndroid {
return "", "", nil, nil, nil
}
case "iOS":
if platform != platformIOS {
return "", "", nil, nil, nil
}
case "":
// Allow any platforms
default:
return "", "", nil, nil, fmt.Errorf("gamepaddb: unexpected platform: %s", tks[1])
}
continue
}
gb, err := parseMappingElement(tks[1])
if err != nil {
return "", "", nil, nil, err
}
if b, ok := toStandardGamepadButton(tks[0]); ok {
if buttons == nil {
buttons = map[StandardButton]*mapping{}
}
buttons[b] = gb
continue
}
if a, ok := toStandardGamepadAxis(tks[0]); ok {
if axes == nil {
axes = map[StandardAxis]*mapping{}
}
axes[a] = gb
continue
}
// The buttons like "misc1" are ignored so far.
// There is no corresponding button in the Web standard gamepad layout.
}
return tokens[0], tokens[1], buttons, axes, nil
}
func parseMappingElement(str string) (*mapping, error) {
switch {
case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"):
var tilda bool
if str[len(str)-1] == '~' {
str = str[:len(str)-1]
tilda = true
}
min := -1
max := 1
numstr := str[1:]
if str[0] == '+' {
numstr = str[2:]
min = 0
} else if str[0] == '-' {
numstr = str[2:]
max = 0
}
scale := 2 / (max - min)
offset := -(max + min)
if tilda {
scale = -scale
offset = -offset
}
index, err := strconv.Atoi(numstr)
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeAxis,
Index: index,
AxisScale: scale,
AxisOffset: offset,
}, nil
case str[0] == 'b':
index, err := strconv.Atoi(str[1:])
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeButton,
Index: index,
}, nil
case str[0] == 'h':
tokens := strings.Split(str[1:], ".")
if len(tokens) < 2 {
return nil, fmt.Errorf("gamepaddb: unexpected hat: %s", str)
}
index, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, err
}
hat, err := strconv.Atoi(tokens[1])
if err != nil {
return nil, err
}
return &mapping{
Type: mappingTypeHat,
Index: index,
HatState: hat,
}, nil
}
return nil, fmt.Errorf("gamepaddb: unepxected mapping: %s", str)
}
func toStandardGamepadButton(str string) (StandardButton, bool) {
switch str {
case "a":
return StandardButtonRightBottom, true
case "b":
return StandardButtonRightRight, true
case "x":
return StandardButtonRightLeft, true
case "y":
return StandardButtonRightTop, true
case "back":
return StandardButtonCenterLeft, true
case "start":
return StandardButtonCenterRight, true
case "guide":
return StandardButtonCenterCenter, true
case "leftshoulder":
return StandardButtonFrontTopLeft, true
case "rightshoulder":
return StandardButtonFrontTopRight, true
case "leftstick":
return StandardButtonLeftStick, true
case "rightstick":
return StandardButtonRightStick, true
case "dpup":
return StandardButtonLeftTop, true
case "dpright":
return StandardButtonLeftRight, true
case "dpdown":
return StandardButtonLeftBottom, true
case "dpleft":
return StandardButtonLeftLeft, true
case "lefttrigger":
return StandardButtonFrontBottomLeft, true
case "righttrigger":
return StandardButtonFrontBottomRight, true
default:
return 0, false
}
}
func toStandardGamepadAxis(str string) (StandardAxis, bool) {
switch str {
case "leftx":
return StandardAxisLeftStickHorizontal, true
case "lefty":
return StandardAxisLeftStickVertical, true
case "rightx":
return StandardAxisRightStickHorizontal, true
case "righty":
return StandardAxisRightStickVertical, true
default:
return 0, false
}
}
func buttonMappings(id string) map[StandardButton]*mapping {
if m, ok := gamepadButtonMappings[id]; ok {
return m
}
if currentPlatform == platformAndroid {
if addAndroidDefaultMappings(id) {
return gamepadButtonMappings[id]
}
}
return nil
}
func axisMappings(id string) map[StandardAxis]*mapping {
if m, ok := gamepadAxisMappings[id]; ok {
return m
}
if currentPlatform == platformAndroid {
if addAndroidDefaultMappings(id) {
return gamepadAxisMappings[id]
}
}
return nil
}
func HasStandardLayoutMapping(id string) bool {
mappingsM.RLock()
defer mappingsM.RUnlock()
return buttonMappings(id) != nil || axisMappings(id) != nil
}
type GamepadState interface {
Axis(index int) float64
Button(index int) bool
Hat(index int) int
}
func Name(id string) string {
mappingsM.RLock()
defer mappingsM.RUnlock()
return gamepadNames[id]
}
func HasStandardAxis(id string, axis StandardAxis) bool {
mappingsM.RLock()
defer mappingsM.RUnlock()
mappings := axisMappings(id)
if mappings == nil {
return false
}
mapping := mappings[axis]
if mapping == nil {
return false
}
return true
}
func AxisValue(id string, axis StandardAxis, state GamepadState) float64 {
mappingsM.RLock()
defer mappingsM.RUnlock()
mappings := axisMappings(id)
if mappings == nil {
return 0
}
mapping := mappings[axis]
if mapping == nil {
return 0
}
switch mapping.Type {
case mappingTypeAxis:
v := state.Axis(mapping.Index)*float64(mapping.AxisScale) + float64(mapping.AxisOffset)
if v > 1 {
return 1
} else if v < -1 {
return -1
}
return v
case mappingTypeButton:
if state.Button(mapping.Index) {
return 1
} else {
return -1
}
case mappingTypeHat:
if state.Hat(mapping.Index)&mapping.HatState != 0 {
return 1
} else {
return -1
}
}
return 0
}
func HasStandardButton(id string, button StandardButton) bool {
mappingsM.RLock()
defer mappingsM.RUnlock()
mappings := buttonMappings(id)
if mappings == nil {
return false
}
mapping := mappings[button]
if mapping == nil {
return false
}
return true
}
func ButtonValue(id string, button StandardButton, state GamepadState) float64 {
mappingsM.RLock()
defer mappingsM.RUnlock()
return buttonValue(id, button, state)
}
func buttonValue(id string, button StandardButton, state GamepadState) float64 {
mappings := buttonMappings(id)
if mappings == nil {
return 0
}
mapping := mappings[button]
if mapping == nil {
return 0
}
switch mapping.Type {
case mappingTypeAxis:
v := state.Axis(mapping.Index)*float64(mapping.AxisScale) + float64(mapping.AxisOffset)
if v > 1 {
v = 1
} else if v < -1 {
v = -1
}
// Adjust [-1, 1] to [0, 1]
return (v + 1) / 2
case mappingTypeButton:
if state.Button(mapping.Index) {
return 1
}
return 0
case mappingTypeHat:
if state.Hat(mapping.Index)&mapping.HatState != 0 {
return 1
}
return 0
}
return 0
}
func IsButtonPressed(id string, button StandardButton, state GamepadState) bool {
// Use XInput's trigger dead zone.
// See https://source.chromium.org/chromium/chromium/src/+/main:device/gamepad/public/cpp/gamepad.h;l=22-23;drc=6997f8a177359bb99598988ed5e900841984d242
const threshold = 30.0 / 255.0
mappingsM.RLock()
defer mappingsM.RUnlock()
mappings, ok := gamepadButtonMappings[id]
if !ok {
return false
}
mapping := mappings[button]
if mapping == nil {
return false
}
switch mapping.Type {
case mappingTypeAxis:
v := buttonValue(id, button, state)
return v > threshold
case mappingTypeButton:
return state.Button(mapping.Index)
case mappingTypeHat:
return state.Hat(mapping.Index)&mapping.HatState != 0
}
return false
}
// Update adds new gamepad mappings.
// The string must be in the format of SDL_GameControllerDB.
//
// Update works atomically. If an error happens, nothing is updated.
func Update(mappingData []byte) error {
mappingsM.Lock()
defer mappingsM.Unlock()
buf := bytes.NewBuffer(mappingData)
s := bufio.NewScanner(buf)
type parsedLine struct {
id string
name string
buttons map[StandardButton]*mapping
axes map[StandardAxis]*mapping
}
var lines []parsedLine
for s.Scan() {
line := s.Text()
id, name, buttons, axes, err := parseLine(line, currentPlatform)
if err != nil {
return err
}
if id != "" {
lines = append(lines, parsedLine{
id: id,
name: name,
buttons: buttons,
axes: axes,
})
}
}
if err := s.Err(); err != nil {
return err
}
for _, l := range lines {
gamepadNames[l.id] = l.name
gamepadButtonMappings[l.id] = l.buttons
gamepadAxisMappings[l.id] = l.axes
}
return nil
}
func addAndroidDefaultMappings(id string) bool {
// See https://github.com/libsdl-org/SDL/blob/120c76c84bbce4c1bfed4e9eb74e10678bd83120/src/joystick/SDL_gamecontroller.c#L468-L568
const faceButtonMask = ((1 << SDLControllerButtonA) |
(1 << SDLControllerButtonB) |
(1 << SDLControllerButtonX) |
(1 << SDLControllerButtonY))
idBytes, err := hex.DecodeString(id)
if err != nil {
return false
}
buttonMask := uint16(idBytes[12]) | (uint16(idBytes[13]) << 8)
axisMask := uint16(idBytes[14]) | (uint16(idBytes[15]) << 8)
if buttonMask == 0 && axisMask == 0 {
return false
}
if buttonMask&faceButtonMask == 0 {
return false
}
gamepadButtonMappings[id] = map[StandardButton]*mapping{}
gamepadAxisMappings[id] = map[StandardAxis]*mapping{}
// For mappings, see mobile/ebitenmobileview/input_android.go.
if buttonMask&(1<<SDLControllerButtonA) != 0 {
gamepadButtonMappings[id][StandardButtonRightBottom] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonA,
}
}
if buttonMask&(1<<SDLControllerButtonB) != 0 {
gamepadButtonMappings[id][StandardButtonRightRight] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonB,
}
} else {
// Use the back button as "B" for easy UI navigation with TV remotes.
gamepadButtonMappings[id][StandardButtonRightRight] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonBack,
}
buttonMask &^= uint16(1) << SDLControllerButtonBack
}
if buttonMask&(1<<SDLControllerButtonX) != 0 {
gamepadButtonMappings[id][StandardButtonRightLeft] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonX,
}
}
if buttonMask&(1<<SDLControllerButtonY) != 0 {
gamepadButtonMappings[id][StandardButtonRightTop] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonY,
}
}
if buttonMask&(1<<SDLControllerButtonBack) != 0 {
gamepadButtonMappings[id][StandardButtonCenterLeft] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonBack,
}
}
if buttonMask&(1<<SDLControllerButtonGuide) != 0 {
// TODO: If SDKVersion >= 30, add this code:
//
// gamepadButtonMappings[id][StandardButtonCenterCenter] = &mapping{
// Type: mappingTypeButton,
// Index: SDLControllerButtonGuide,
// }
}
if buttonMask&(1<<SDLControllerButtonStart) != 0 {
gamepadButtonMappings[id][StandardButtonCenterRight] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonStart,
}
}
if buttonMask&(1<<SDLControllerButtonLeftStick) != 0 {
gamepadButtonMappings[id][StandardButtonLeftStick] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonLeftStick,
}
}
if buttonMask&(1<<SDLControllerButtonRightStick) != 0 {
gamepadButtonMappings[id][StandardButtonRightStick] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonRightStick,
}
}
if buttonMask&(1<<SDLControllerButtonLeftShoulder) != 0 {
gamepadButtonMappings[id][StandardButtonFrontTopLeft] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonLeftShoulder,
}
}
if buttonMask&(1<<SDLControllerButtonRightShoulder) != 0 {
gamepadButtonMappings[id][StandardButtonFrontTopRight] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonRightShoulder,
}
}
if buttonMask&(1<<SDLControllerButtonDpadUp) != 0 {
gamepadButtonMappings[id][StandardButtonLeftTop] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadUp,
}
}
if buttonMask&(1<<SDLControllerButtonDpadDown) != 0 {
gamepadButtonMappings[id][StandardButtonLeftBottom] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadDown,
}
}
if buttonMask&(1<<SDLControllerButtonDpadLeft) != 0 {
gamepadButtonMappings[id][StandardButtonLeftLeft] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadLeft,
}
}
if buttonMask&(1<<SDLControllerButtonDpadRight) != 0 {
gamepadButtonMappings[id][StandardButtonLeftRight] = &mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadRight,
}
}
if axisMask&(1<<SDLControllerAxisLeftX) != 0 {
gamepadAxisMappings[id][StandardAxisLeftStickHorizontal] = &mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisLeftX,
AxisScale: 1,
AxisOffset: 0,
}
}
if axisMask&(1<<SDLControllerAxisLeftY) != 0 {
gamepadAxisMappings[id][StandardAxisLeftStickVertical] = &mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisLeftY,
AxisScale: 1,
AxisOffset: 0,
}
}
if axisMask&(1<<SDLControllerAxisRightX) != 0 {
gamepadAxisMappings[id][StandardAxisRightStickHorizontal] = &mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisRightX,
AxisScale: 1,
AxisOffset: 0,
}
}
if axisMask&(1<<SDLControllerAxisRightY) != 0 {
gamepadAxisMappings[id][StandardAxisRightStickVertical] = &mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisRightY,
AxisScale: 1,
AxisOffset: 0,
}
}
if axisMask&(1<<SDLControllerAxisTriggerLeft) != 0 {
gamepadButtonMappings[id][StandardButtonFrontBottomLeft] = &mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisTriggerLeft,
AxisScale: 1,
AxisOffset: 0,
}
}
if axisMask&(1<<SDLControllerAxisTriggerRight) != 0 {
gamepadButtonMappings[id][StandardButtonFrontBottomRight] = &mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisTriggerRight,
AxisScale: 1,
AxisOffset: 0,
}
}
return true
}