File size: 5,594 Bytes
d82cf6a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Game Controller support.

This module provides an interface for Game Controller devices, which are a
subset of Joysticks. Game Controllers have consistent button and axis mapping,
which resembles common dual-stick home video game console controllers.
Devices that are of this design can be automatically mapped to the "virtual"
Game Controller layout, providing a consistent abstraction for a large number
of different devices, with no tedious button and axis mapping for each one.
To achieve this, an internal mapping database contains lists of device ids
and their corresponding button and axis mappings. The mapping database is in
the same format as originated by the `SDL` library, which has become a
semi-standard and is in common use. Most popular controllers are included in
the built-in database, and additional mappings can be added at runtime.


Some Joysticks, such as Flight Sticks, etc., do not necessarily fit into the
layout (and limitations) of GameControllers. For those such devices, it is
recommended to use the Joystick interface instead.

To query which GameControllers are available, call :py:func:`get_controllers`.

.. versionadded:: 2.0
"""
import os as _os
import sys as _sys
import warnings as _warnings

from .controller_db import mapping_list


_env_config = _os.environ.get('SDL_GAMECONTROLLERCONFIG')
if _env_config:
    # insert at the front of the list
    mapping_list.insert(0, _env_config)


def _swap_le16(value):
    """Ensure 16bit value is in Big Endian format"""
    if _sys.byteorder == "little":
        return ((value << 8) | (value >> 8)) & 0xFFFF
    return value


def create_guid(bus: int, vendor: int, product: int, version: int, name: str, signature: int, data: int) -> str:
    # byte size      16           16            16            16         str             8          8
    """Create an SDL2 style GUID string from a device's identifiers."""
    bus = _swap_le16(bus)
    vendor = _swap_le16(vendor)
    product = _swap_le16(product)
    version = _swap_le16(version)

    return f"{bus:04x}0000{vendor:04x}0000{product:04x}0000{version:04x}0000"


class Relation:
    __slots__ = 'control_type', 'index', 'inverted'

    def __init__(self, control_type, index, inverted=False):
        self.control_type = control_type
        self.index = index
        self.inverted = inverted

    def __repr__(self):
        return f"Relation(type={self.control_type}, index={self.index}, inverted={self.inverted})"


def _parse_mapping(mapping_string):
    """Parse a SDL2 style GameController mapping string.

    :Parameters:
        `mapping_string` : str
            A raw string containing an SDL style controller mapping.

    :rtype: A dict containing axis/button mapping relations.
    """

    valid_keys = ['guide', 'back', 'start', 'a', 'b', 'x', 'y',
                  'leftshoulder', 'leftstick', 'rightshoulder', 'rightstick',
                  'dpup', 'dpdown', 'dpleft', 'dpright',
                  'lefttrigger', 'righttrigger', 'leftx', 'lefty', 'rightx', 'righty']

    split_mapping = mapping_string.strip().split(",")
    relations = dict(guid=split_mapping[0], name=split_mapping[1])

    for item in split_mapping[2:]:
        # looking for items like: a:b0, b:b1, etc.
        if ':' not in item:
            continue

        key, relation_string, *etc = item.split(':')

        if key not in valid_keys:
            continue

        # Look for specific flags to signify inverted axis:
        if "+" in relation_string:
            relation_string = relation_string.strip('+')
            inverted = False
        elif "-" in relation_string:
            relation_string = relation_string.strip('-')
            inverted = True
        elif "~" in relation_string:
            relation_string = relation_string.strip('~')
            inverted = True
        else:
            inverted = False

        # All relations will be one of (Button, Axis, or Hat).
        if relation_string.startswith("b"):  # Button
            relations[key] = Relation("button", int(relation_string[1:]), inverted)
        elif relation_string.startswith("a"):  # Axis
            relations[key] = Relation("axis", int(relation_string[1:]), inverted)
        elif relation_string.startswith("h0"):  # Hat
            relations[key] = Relation("hat0", int(relation_string.split(".")[1]), inverted)

    return relations


def get_mapping(guid):
    """Return a mapping for the passed device GUID.

    :Parameters:
        `guid` : str
            A pyglet input device GUID

    :rtype: dict of axis/button mapping relations, or None
            if no mapping is available for this Controller.
    """
    for mapping in mapping_list:
        if mapping.startswith(guid):
            try:
                return _parse_mapping(mapping)
            except ValueError:
                _warnings.warn(f"Unable to parse Controller mapping: {mapping}")
                continue


def add_mappings_from_file(filename) -> None:
    """Add mappings from a file.

    Given a file path, open and parse the file for mappings.

    :Parameters:
        `filename` : str
            A file path.
    """
    with open(filename) as f:
        add_mappings_from_string(f.read())


def add_mappings_from_string(string) -> None:
    """Add one or more mappings from a raw string.

        :Parameters:
            `string` : str
                A string containing one or more mappings,
        """
    for line in string.splitlines():
        if line.startswith('#'):
            continue
        line = line.strip()
        mapping_list.append(line)