gamepaddb.go (12751B)
1 // Copyright 2021 The Ebiten Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // gamecontrollerdb.txt is downloaded at https://github.com/gabomdq/SDL_GameControllerDB. 16 17 // To update the database file, run: 18 // 19 // curl --location --remote-name https://raw.githubusercontent.com/gabomdq/SDL_GameControllerDB/master/gamecontrollerdb.txt 20 21 //go:generate file2byteslice -package gamepaddb -input=./gamecontrollerdb.txt -output=./gamecontrollerdb.txt.go -var=gamecontrollerdbTxt 22 23 package gamepaddb 24 25 import ( 26 "bufio" 27 "bytes" 28 "fmt" 29 "io" 30 "runtime" 31 "strconv" 32 "strings" 33 "sync" 34 35 "github.com/hajimehoshi/ebiten/v2/internal/driver" 36 ) 37 38 type platform int 39 40 const ( 41 platformUnknown platform = iota 42 platformWindows 43 platformMacOS 44 platformUnix 45 platformAndroid 46 platformIOS 47 ) 48 49 var currentPlatform platform 50 51 func init() { 52 if runtime.GOOS == "windows" { 53 currentPlatform = platformWindows 54 return 55 } 56 57 if runtime.GOOS == "aix" || 58 runtime.GOOS == "dragonfly" || 59 runtime.GOOS == "freebsd" || 60 runtime.GOOS == "hurd" || 61 runtime.GOOS == "illumos" || 62 runtime.GOOS == "linux" || 63 runtime.GOOS == "netbsd" || 64 runtime.GOOS == "openbsd" || 65 runtime.GOOS == "solaris" { 66 currentPlatform = platformUnix 67 return 68 } 69 70 if runtime.GOOS == "android" { 71 currentPlatform = platformAndroid 72 return 73 } 74 75 if isIOS { 76 currentPlatform = platformIOS 77 return 78 } 79 80 if runtime.GOOS == "darwin" { 81 currentPlatform = platformMacOS 82 return 83 } 84 } 85 86 var additionalGLFWGamepads = []byte(` 87 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, 88 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, 89 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, 90 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, 91 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, 92 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, 93 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, 94 `) 95 96 func init() { 97 if _, err := Update(gamecontrollerdbTxt); err != nil { 98 panic(err) 99 } 100 if _, err := Update(additionalGLFWGamepads); err != nil { 101 panic(err) 102 } 103 } 104 105 type mappingType int 106 107 const ( 108 mappingTypeButton mappingType = iota 109 mappingTypeAxis 110 mappingTypeHat 111 ) 112 113 const ( 114 HatUp = 1 115 HatRight = 2 116 HatDown = 4 117 HatLeft = 8 118 ) 119 120 type mapping struct { 121 Type mappingType 122 Index int 123 AxisScale int 124 AxisOffset int 125 HatState int 126 } 127 128 var ( 129 gamepadButtonMappings = map[string]map[driver.StandardGamepadButton]*mapping{} 130 gamepadAxisMappings = map[string]map[driver.StandardGamepadAxis]*mapping{} 131 mappingsM sync.RWMutex 132 ) 133 134 func processLine(line string, platform platform) error { 135 line = strings.TrimSpace(line) 136 if len(line) == 0 { 137 return nil 138 } 139 if line[0] == '#' { 140 return nil 141 } 142 tokens := strings.Split(line, ",") 143 id := tokens[0] 144 for _, token := range tokens[2:] { 145 if len(token) == 0 { 146 continue 147 } 148 tks := strings.Split(token, ":") 149 if tks[0] == "platform" { 150 switch tks[1] { 151 case "Windows": 152 if platform != platformWindows { 153 return nil 154 } 155 case "Mac OS X": 156 if platform != platformMacOS { 157 return nil 158 } 159 case "Linux": 160 if platform != platformUnix { 161 return nil 162 } 163 case "Android": 164 if platform != platformAndroid { 165 return nil 166 } 167 case "iOS": 168 if platform != platformIOS { 169 return nil 170 } 171 default: 172 return fmt.Errorf("gamepaddb: unexpected platform: %s", tks[1]) 173 } 174 continue 175 } 176 177 gb, err := parseMappingElement(tks[1]) 178 if err != nil { 179 return err 180 } 181 182 if b, ok := toStandardGamepadButton(tks[0]); ok { 183 m, ok := gamepadButtonMappings[id] 184 if !ok { 185 m = map[driver.StandardGamepadButton]*mapping{} 186 gamepadButtonMappings[id] = m 187 } 188 m[b] = gb 189 continue 190 } 191 192 if a, ok := toStandardGamepadAxis(tks[0]); ok { 193 m, ok := gamepadAxisMappings[id] 194 if !ok { 195 m = map[driver.StandardGamepadAxis]*mapping{} 196 gamepadAxisMappings[id] = m 197 } 198 m[a] = gb 199 continue 200 } 201 202 // The buttons like "misc1" are ignored so far. 203 // There is no corresponding button in the Web standard gamepad layout. 204 } 205 206 return nil 207 } 208 209 func parseMappingElement(str string) (*mapping, error) { 210 switch { 211 case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"): 212 var tilda bool 213 if str[len(str)-1] == '~' { 214 str = str[:len(str)-1] 215 tilda = true 216 } 217 218 min := -1 219 max := 1 220 numstr := str[1:] 221 222 if str[0] == '+' { 223 numstr = str[2:] 224 min = 0 225 } else if str[0] == '-' { 226 numstr = str[2:] 227 max = 0 228 } 229 230 scale := 2 / (max - min) 231 offset := -(max + min) 232 if tilda { 233 scale = -scale 234 offset = -offset 235 } 236 237 index, err := strconv.Atoi(numstr) 238 if err != nil { 239 return nil, err 240 } 241 242 return &mapping{ 243 Type: mappingTypeAxis, 244 Index: index, 245 AxisScale: scale, 246 AxisOffset: offset, 247 }, nil 248 249 case str[0] == 'b': 250 index, err := strconv.Atoi(str[1:]) 251 if err != nil { 252 return nil, err 253 } 254 return &mapping{ 255 Type: mappingTypeButton, 256 Index: index, 257 }, nil 258 259 case str[0] == 'h': 260 tokens := strings.Split(str[1:], ".") 261 if len(tokens) < 2 { 262 return nil, fmt.Errorf("gamepaddb: unexpected hat: %s", str) 263 } 264 index, err := strconv.Atoi(tokens[0]) 265 if err != nil { 266 return nil, err 267 } 268 hat, err := strconv.Atoi(tokens[1]) 269 if err != nil { 270 return nil, err 271 } 272 return &mapping{ 273 Type: mappingTypeHat, 274 Index: index, 275 HatState: hat, 276 }, nil 277 } 278 279 return nil, fmt.Errorf("gamepaddb: unepxected mapping: %s", str) 280 } 281 282 func toStandardGamepadButton(str string) (driver.StandardGamepadButton, bool) { 283 switch str { 284 case "a": 285 return driver.StandardGamepadButtonRightBottom, true 286 case "b": 287 return driver.StandardGamepadButtonRightRight, true 288 case "x": 289 return driver.StandardGamepadButtonRightLeft, true 290 case "y": 291 return driver.StandardGamepadButtonRightTop, true 292 case "back": 293 return driver.StandardGamepadButtonCenterLeft, true 294 case "start": 295 return driver.StandardGamepadButtonCenterRight, true 296 case "guide": 297 return driver.StandardGamepadButtonCenterCenter, true 298 case "leftshoulder": 299 return driver.StandardGamepadButtonFrontTopLeft, true 300 case "rightshoulder": 301 return driver.StandardGamepadButtonFrontTopRight, true 302 case "leftstick": 303 return driver.StandardGamepadButtonLeftStick, true 304 case "rightstick": 305 return driver.StandardGamepadButtonRightStick, true 306 case "dpup": 307 return driver.StandardGamepadButtonLeftTop, true 308 case "dpright": 309 return driver.StandardGamepadButtonLeftRight, true 310 case "dpdown": 311 return driver.StandardGamepadButtonLeftBottom, true 312 case "dpleft": 313 return driver.StandardGamepadButtonLeftLeft, true 314 case "lefttrigger": 315 return driver.StandardGamepadButtonFrontBottomLeft, true 316 case "righttrigger": 317 return driver.StandardGamepadButtonFrontBottomRight, true 318 default: 319 return 0, false 320 } 321 } 322 323 func toStandardGamepadAxis(str string) (driver.StandardGamepadAxis, bool) { 324 switch str { 325 case "leftx": 326 return driver.StandardGamepadAxisLeftStickHorizontal, true 327 case "lefty": 328 return driver.StandardGamepadAxisLeftStickVertical, true 329 case "rightx": 330 return driver.StandardGamepadAxisRightStickHorizontal, true 331 case "righty": 332 return driver.StandardGamepadAxisRightStickVertical, true 333 default: 334 return 0, false 335 } 336 } 337 338 func HasStandardLayoutMapping(id string) bool { 339 mappingsM.RLock() 340 defer mappingsM.RUnlock() 341 342 if _, ok := gamepadButtonMappings[id]; ok { 343 return true 344 } 345 if _, ok := gamepadAxisMappings[id]; ok { 346 return true 347 } 348 return false 349 } 350 351 type GamepadState interface { 352 Axis(index int) float64 353 Button(index int) bool 354 Hat(index int) int 355 } 356 357 func AxisValue(id string, axis driver.StandardGamepadAxis, state GamepadState) float64 { 358 mappingsM.RLock() 359 defer mappingsM.RUnlock() 360 361 mappings, ok := gamepadAxisMappings[id] 362 if !ok { 363 return 0 364 } 365 366 mapping := mappings[axis] 367 if mapping == nil { 368 return 0 369 } 370 371 switch mapping.Type { 372 case mappingTypeAxis: 373 v := state.Axis(mapping.Index)*float64(mapping.AxisScale) + float64(mapping.AxisOffset) 374 if v > 1 { 375 return 1 376 } else if v < -1 { 377 return -1 378 } 379 return v 380 case mappingTypeButton: 381 if state.Button(mapping.Index) { 382 return 1 383 } else { 384 return -1 385 } 386 case mappingTypeHat: 387 if state.Hat(mapping.Index)&mapping.HatState != 0 { 388 return 1 389 } else { 390 return -1 391 } 392 } 393 394 return 0 395 } 396 397 func ButtonValue(id string, button driver.StandardGamepadButton, state GamepadState) float64 { 398 mappingsM.RLock() 399 defer mappingsM.RUnlock() 400 401 return buttonValue(id, button, state) 402 } 403 404 func buttonValue(id string, button driver.StandardGamepadButton, state GamepadState) float64 { 405 mappings, ok := gamepadButtonMappings[id] 406 if !ok { 407 return 0 408 } 409 410 mapping := mappings[button] 411 if mapping == nil { 412 return 0 413 } 414 415 switch mapping.Type { 416 case mappingTypeAxis: 417 v := state.Axis(mapping.Index)*float64(mapping.AxisScale) + float64(mapping.AxisOffset) 418 if v > 1 { 419 v = 1 420 } else if v < -1 { 421 v = -1 422 } 423 // Adjust [-1, 1] to [0, 1] 424 return (v + 1) / 2 425 case mappingTypeButton: 426 if state.Button(mapping.Index) { 427 return 1 428 } 429 return 0 430 case mappingTypeHat: 431 if state.Hat(mapping.Index)&mapping.HatState != 0 { 432 return 1 433 } 434 return 0 435 } 436 437 return 0 438 } 439 440 func IsButtonPressed(id string, button driver.StandardGamepadButton, state GamepadState) bool { 441 // Use XInput's trigger dead zone. 442 // See https://source.chromium.org/chromium/chromium/src/+/main:device/gamepad/public/cpp/gamepad.h;l=22-23;drc=6997f8a177359bb99598988ed5e900841984d242 443 const threshold = 30.0 / 255.0 444 445 mappingsM.RLock() 446 defer mappingsM.RUnlock() 447 448 mappings, ok := gamepadButtonMappings[id] 449 if !ok { 450 return false 451 } 452 453 mapping := mappings[button] 454 if mapping == nil { 455 return false 456 } 457 458 switch mapping.Type { 459 case mappingTypeAxis: 460 v := buttonValue(id, button, state) 461 return v > threshold 462 case mappingTypeButton: 463 return state.Button(mapping.Index) 464 case mappingTypeHat: 465 return state.Hat(mapping.Index)&mapping.HatState != 0 466 } 467 468 return false 469 } 470 471 // Update adds new gamepad mappings. 472 // The string must be in the format of SDL_GameControllerDB. 473 func Update(mapping []byte) (bool, error) { 474 if currentPlatform == platformUnknown { 475 return false, nil 476 } 477 478 // TODO: Implement this (#1557) 479 if currentPlatform == platformAndroid || currentPlatform == platformIOS { 480 // Note: NOT returning an error, as mappings also do not matter right now. 481 return false, nil 482 } 483 484 mappingsM.Lock() 485 defer mappingsM.Unlock() 486 487 buf := bytes.NewBuffer(mapping) 488 r := bufio.NewReader(buf) 489 for { 490 line, err := r.ReadString('\n') 491 if err != nil && err != io.EOF { 492 return false, err 493 } 494 if err := processLine(line, currentPlatform); err != nil { 495 return false, err 496 } 497 if err == io.EOF { 498 break 499 } 500 } 501 502 return true, nil 503 }