emoemu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import { StandardButton } from '../../core/button';
|
|
2
|
+
import {
|
|
3
|
+
readInt16LE,
|
|
4
|
+
readUint16LE,
|
|
5
|
+
applySignedAnalogToDpad,
|
|
6
|
+
analogToDpad,
|
|
7
|
+
hatToDpad,
|
|
8
|
+
} from '../../utils/buffer';
|
|
9
|
+
import {
|
|
10
|
+
// Vendor IDs
|
|
11
|
+
VENDOR_MICROSOFT,
|
|
12
|
+
VENDOR_SONY,
|
|
13
|
+
VENDOR_NINTENDO,
|
|
14
|
+
VENDOR_8BITDO,
|
|
15
|
+
VENDOR_PDP,
|
|
16
|
+
VENDOR_HORI,
|
|
17
|
+
VENDOR_RAZER,
|
|
18
|
+
VENDOR_POWERA,
|
|
19
|
+
VENDOR_VALVE,
|
|
20
|
+
VENDOR_LOGITECH,
|
|
21
|
+
VENDOR_DRAGONRISE,
|
|
22
|
+
VENDOR_GENERIC_0810,
|
|
23
|
+
VENDOR_HONEYBEE,
|
|
24
|
+
VENDOR_GENERIC_1A34,
|
|
25
|
+
VENDOR_POWERA_BDA,
|
|
26
|
+
// Xbox 360 Product IDs
|
|
27
|
+
PRODUCT_XBOX_360,
|
|
28
|
+
PRODUCT_XBOX_360_WIRELESS,
|
|
29
|
+
PRODUCT_XBOX_360_WIRELESS_RECEIVER,
|
|
30
|
+
PRODUCT_XBOX_360_WIRELESS_ALT,
|
|
31
|
+
PRODUCT_XBOX_360_WIRELESS_RECEIVER_ALT,
|
|
32
|
+
// Xbox One/Series Product IDs
|
|
33
|
+
PRODUCT_XBOX_ONE,
|
|
34
|
+
PRODUCT_XBOX_ONE_2015,
|
|
35
|
+
PRODUCT_XBOX_ONE_ELITE,
|
|
36
|
+
PRODUCT_XBOX_ONE_S,
|
|
37
|
+
PRODUCT_XBOX_ONE_S_BT,
|
|
38
|
+
PRODUCT_XBOX_ELITE_S2,
|
|
39
|
+
PRODUCT_XBOX_ELITE_S2_BT,
|
|
40
|
+
PRODUCT_XBOX_SERIES,
|
|
41
|
+
PRODUCT_XBOX_SERIES_BT,
|
|
42
|
+
PRODUCT_XBOX_ADAPTIVE,
|
|
43
|
+
PRODUCT_XBOX_ADAPTIVE_BT,
|
|
44
|
+
PRODUCT_XBOX_ELITE_S2_V2,
|
|
45
|
+
// PlayStation Product IDs
|
|
46
|
+
PRODUCT_DUALSHOCK4_V1,
|
|
47
|
+
PRODUCT_DUALSHOCK4_V2,
|
|
48
|
+
PRODUCT_DUALSHOCK4_ADAPTER,
|
|
49
|
+
PRODUCT_DUALSENSE,
|
|
50
|
+
PRODUCT_DUALSENSE_EDGE,
|
|
51
|
+
// Nintendo Product IDs
|
|
52
|
+
PRODUCT_SWITCH_PRO,
|
|
53
|
+
PRODUCT_SNES_CONTROLLER,
|
|
54
|
+
PRODUCT_N64_CONTROLLER,
|
|
55
|
+
PRODUCT_GENESIS_CONTROLLER,
|
|
56
|
+
// 8BitDo Product IDs
|
|
57
|
+
PRODUCT_8BITDO_SN30,
|
|
58
|
+
PRODUCT_8BITDO_SN30_PRO,
|
|
59
|
+
PRODUCT_8BITDO_SN30_PRO_PLUS,
|
|
60
|
+
PRODUCT_8BITDO_PRO_2,
|
|
61
|
+
PRODUCT_8BITDO_ULTIMATE,
|
|
62
|
+
PRODUCT_8BITDO_SF30,
|
|
63
|
+
PRODUCT_8BITDO_SF30_PRO,
|
|
64
|
+
// Xbox HID constants
|
|
65
|
+
XBOX_WIRED_REPORT_TYPE,
|
|
66
|
+
XBOX_WIRED_REPORT_LENGTH,
|
|
67
|
+
XBOX_SERIES_BT_REPORT_TYPE,
|
|
68
|
+
XBOX_SERIES_BT_REPORT_LENGTH,
|
|
69
|
+
XBOX_WIRED_BUTTONS_BYTE,
|
|
70
|
+
XBOX_WIRED_DPAD_BYTE,
|
|
71
|
+
XBOX_WIRED_SHOULDERS_BYTE,
|
|
72
|
+
XBOX_WIRED_LEFT_STICK_X_OFFSET,
|
|
73
|
+
XBOX_WIRED_LEFT_STICK_Y_OFFSET,
|
|
74
|
+
XBOX_WIRED_RIGHT_STICK_X_OFFSET,
|
|
75
|
+
XBOX_WIRED_RIGHT_STICK_Y_OFFSET,
|
|
76
|
+
XBOX_WIRED_MASK_START,
|
|
77
|
+
XBOX_WIRED_MASK_SELECT,
|
|
78
|
+
XBOX_WIRED_MASK_A,
|
|
79
|
+
XBOX_WIRED_MASK_B,
|
|
80
|
+
XBOX_WIRED_MASK_X,
|
|
81
|
+
XBOX_WIRED_MASK_Y,
|
|
82
|
+
XBOX_WIRED_MASK_GUIDE_1,
|
|
83
|
+
XBOX_WIRED_MASK_GUIDE_2,
|
|
84
|
+
XBOX_SERIES_BT_LEFT_X_OFFSET,
|
|
85
|
+
XBOX_SERIES_BT_LEFT_Y_OFFSET,
|
|
86
|
+
XBOX_SERIES_BT_RIGHT_X_OFFSET,
|
|
87
|
+
XBOX_SERIES_BT_RIGHT_Y_OFFSET,
|
|
88
|
+
XBOX_SERIES_BT_HAT_BYTE,
|
|
89
|
+
XBOX_SERIES_BT_FACE_BUTTONS_BYTE,
|
|
90
|
+
XBOX_SERIES_BT_MENU_BUTTONS_BYTE,
|
|
91
|
+
XBOX_SERIES_BT_MASK_A,
|
|
92
|
+
XBOX_SERIES_BT_MASK_B,
|
|
93
|
+
XBOX_SERIES_BT_MASK_X,
|
|
94
|
+
XBOX_SERIES_BT_MASK_Y,
|
|
95
|
+
XBOX_SERIES_BT_MASK_LB,
|
|
96
|
+
XBOX_SERIES_BT_MASK_RB,
|
|
97
|
+
XBOX_SERIES_BT_MASK_VIEW,
|
|
98
|
+
XBOX_SERIES_BT_MASK_MENU,
|
|
99
|
+
XBOX_SERIES_BT_MASK_XBOX,
|
|
100
|
+
XBOX_SERIES_BT_ANALOG_LOW,
|
|
101
|
+
XBOX_SERIES_BT_ANALOG_HIGH,
|
|
102
|
+
// Xbox 360 constants
|
|
103
|
+
XBOX_360_MIN_REPORT_LENGTH,
|
|
104
|
+
XBOX_360_BUTTONS_BYTE_1,
|
|
105
|
+
XBOX_360_BUTTONS_BYTE_2,
|
|
106
|
+
XBOX_360_MASK_DPAD_UP,
|
|
107
|
+
XBOX_360_MASK_DPAD_DOWN,
|
|
108
|
+
XBOX_360_MASK_DPAD_LEFT,
|
|
109
|
+
XBOX_360_MASK_DPAD_RIGHT,
|
|
110
|
+
XBOX_360_MASK_START,
|
|
111
|
+
XBOX_360_MASK_BACK,
|
|
112
|
+
XBOX_360_MASK_LB,
|
|
113
|
+
XBOX_360_MASK_RB,
|
|
114
|
+
XBOX_360_MASK_A,
|
|
115
|
+
XBOX_360_MASK_B,
|
|
116
|
+
XBOX_360_MASK_X,
|
|
117
|
+
XBOX_360_MASK_Y,
|
|
118
|
+
// D-pad masks
|
|
119
|
+
DPAD_MASK_UP,
|
|
120
|
+
DPAD_MASK_DOWN,
|
|
121
|
+
DPAD_MASK_LEFT,
|
|
122
|
+
DPAD_MASK_RIGHT,
|
|
123
|
+
DPAD_HAT_MASK,
|
|
124
|
+
// Shoulder masks
|
|
125
|
+
SHOULDER_MASK_L,
|
|
126
|
+
SHOULDER_MASK_R,
|
|
127
|
+
SHOULDER_MASK_L2,
|
|
128
|
+
SHOULDER_MASK_R2,
|
|
129
|
+
// PlayStation constants
|
|
130
|
+
PS_REPORT_ID,
|
|
131
|
+
PS_MIN_REPORT_LENGTH,
|
|
132
|
+
PS_ANALOG_CENTER,
|
|
133
|
+
PS_ANALOG_RANGE,
|
|
134
|
+
DS4_LEFT_X_OFFSET,
|
|
135
|
+
DS4_LEFT_Y_OFFSET,
|
|
136
|
+
DS4_RIGHT_X_OFFSET,
|
|
137
|
+
DS4_RIGHT_Y_OFFSET,
|
|
138
|
+
DS4_HAT_AND_BUTTONS_OFFSET,
|
|
139
|
+
DS4_SHOULDERS_OFFSET,
|
|
140
|
+
DS4_PS_BUTTON_OFFSET,
|
|
141
|
+
DS4_MASK_SQUARE,
|
|
142
|
+
DS4_MASK_CROSS,
|
|
143
|
+
DS4_MASK_CIRCLE,
|
|
144
|
+
DS4_MASK_TRIANGLE,
|
|
145
|
+
DUALSENSE_LEFT_X_OFFSET,
|
|
146
|
+
DUALSENSE_LEFT_Y_OFFSET,
|
|
147
|
+
DUALSENSE_RIGHT_X_OFFSET,
|
|
148
|
+
DUALSENSE_RIGHT_Y_OFFSET,
|
|
149
|
+
DUALSENSE_HAT_AND_BUTTONS_OFFSET,
|
|
150
|
+
DUALSENSE_SHOULDERS_OFFSET,
|
|
151
|
+
DUALSENSE_PS_BUTTON_OFFSET,
|
|
152
|
+
DUALSENSE_PS_BUTTON_MASK,
|
|
153
|
+
DUALSENSE_PS_BUTTON_MIN_LENGTH,
|
|
154
|
+
// Switch Pro constants
|
|
155
|
+
SWITCH_PRO_MIN_REPORT_LENGTH,
|
|
156
|
+
SWITCH_PRO_BUTTONS_BYTE_1,
|
|
157
|
+
SWITCH_PRO_BUTTONS_BYTE_2,
|
|
158
|
+
SWITCH_PRO_BUTTONS_BYTE_3,
|
|
159
|
+
SWITCH_PRO_MASK_B,
|
|
160
|
+
SWITCH_PRO_MASK_Y,
|
|
161
|
+
SWITCH_PRO_MASK_A,
|
|
162
|
+
SWITCH_PRO_MASK_X,
|
|
163
|
+
SWITCH_PRO_MASK_L,
|
|
164
|
+
SWITCH_PRO_MASK_R,
|
|
165
|
+
SWITCH_PRO_MASK_ZL,
|
|
166
|
+
SWITCH_PRO_MASK_ZR,
|
|
167
|
+
SWITCH_PRO_MASK_MINUS,
|
|
168
|
+
SWITCH_PRO_MASK_PLUS,
|
|
169
|
+
SWITCH_PRO_MASK_HOME,
|
|
170
|
+
// 8BitDo constants
|
|
171
|
+
EIGHTBITDO_MIN_REPORT_LENGTH,
|
|
172
|
+
EIGHTBITDO_LEFT_X_OFFSET,
|
|
173
|
+
EIGHTBITDO_LEFT_Y_OFFSET,
|
|
174
|
+
EIGHTBITDO_BUTTONS_BYTE_1,
|
|
175
|
+
EIGHTBITDO_BUTTONS_BYTE_2,
|
|
176
|
+
EIGHTBITDO_MASK_DPAD_UP,
|
|
177
|
+
EIGHTBITDO_MASK_DPAD_DOWN,
|
|
178
|
+
EIGHTBITDO_MASK_DPAD_LEFT,
|
|
179
|
+
EIGHTBITDO_MASK_DPAD_RIGHT,
|
|
180
|
+
EIGHTBITDO_MASK_L,
|
|
181
|
+
EIGHTBITDO_MASK_R,
|
|
182
|
+
EIGHTBITDO_MASK_B,
|
|
183
|
+
EIGHTBITDO_MASK_A,
|
|
184
|
+
EIGHTBITDO_MASK_Y,
|
|
185
|
+
EIGHTBITDO_MASK_X,
|
|
186
|
+
EIGHTBITDO_MASK_SELECT,
|
|
187
|
+
EIGHTBITDO_MASK_START,
|
|
188
|
+
// Generic constants
|
|
189
|
+
GENERIC_MIN_REPORT_LENGTH,
|
|
190
|
+
GENERIC_FORMAT_A_MIN_LENGTH,
|
|
191
|
+
GENERIC_ANALOG_OFFSET_X,
|
|
192
|
+
GENERIC_ANALOG_OFFSET_Y,
|
|
193
|
+
GENERIC_HAT_BYTE,
|
|
194
|
+
GENERIC_BUTTONS_BYTE_1,
|
|
195
|
+
GENERIC_BUTTONS_BYTE_2,
|
|
196
|
+
GENERIC_MAX_HAT_VALUE,
|
|
197
|
+
GENERIC_MASK_BUTTON_1,
|
|
198
|
+
GENERIC_MASK_BUTTON_2,
|
|
199
|
+
GENERIC_MASK_BUTTON_3,
|
|
200
|
+
GENERIC_MASK_BUTTON_4,
|
|
201
|
+
GENERIC_MASK_BUTTON_L,
|
|
202
|
+
GENERIC_MASK_BUTTON_R,
|
|
203
|
+
GENERIC_MASK_BUTTON_SELECT,
|
|
204
|
+
GENERIC_MASK_BUTTON_START,
|
|
205
|
+
GENERIC_FALLBACK_MASK_SELECT,
|
|
206
|
+
GENERIC_FALLBACK_MASK_START,
|
|
207
|
+
GENERIC_FALLBACK_MASK_UP,
|
|
208
|
+
GENERIC_FALLBACK_MASK_DOWN,
|
|
209
|
+
GENERIC_FALLBACK_MASK_LEFT,
|
|
210
|
+
GENERIC_FALLBACK_MASK_RIGHT,
|
|
211
|
+
// HID usage constants
|
|
212
|
+
HID_USAGE_PAGE_GENERIC_DESKTOP,
|
|
213
|
+
HID_USAGE_JOYSTICK,
|
|
214
|
+
HID_USAGE_GAMEPAD,
|
|
215
|
+
// Analog input constants
|
|
216
|
+
ANALOG_INT16_MAX,
|
|
217
|
+
ANALOG_UINT16_CENTER,
|
|
218
|
+
} from '..';
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Analog stick state from parsing HID report
|
|
222
|
+
* Values are normalized from -1.0 to 1.0
|
|
223
|
+
*/
|
|
224
|
+
export interface AnalogState {
|
|
225
|
+
/** Left stick X axis (-1.0 = left, 1.0 = right) */
|
|
226
|
+
leftX: number;
|
|
227
|
+
/** Left stick Y axis (-1.0 = up, 1.0 = down) */
|
|
228
|
+
leftY: number;
|
|
229
|
+
/** Right stick X axis (-1.0 = left, 1.0 = right) */
|
|
230
|
+
rightX: number;
|
|
231
|
+
/** Right stick Y axis (-1.0 = up, 1.0 = down) */
|
|
232
|
+
rightY: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Gamepad button mapping profile
|
|
237
|
+
* Maps raw HID report data to StandardButton for multi-core support
|
|
238
|
+
*/
|
|
239
|
+
export interface GamepadProfile {
|
|
240
|
+
name: string;
|
|
241
|
+
/** Vendor IDs this profile matches */
|
|
242
|
+
vendorIds: number[];
|
|
243
|
+
/** Product IDs this profile matches (empty = match any for this vendor) */
|
|
244
|
+
productIds: number[];
|
|
245
|
+
/** Parse HID report and return button states */
|
|
246
|
+
parseReport: (data: Buffer) => Map<StandardButton, boolean>;
|
|
247
|
+
/** Optional: Parse HID report and return analog stick values (normalized -1.0 to 1.0) */
|
|
248
|
+
parseAnalog?: (data: Buffer) => AnalogState | null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Xbox Wired Controller HID format
|
|
253
|
+
* 19-byte reports starting with 0x20
|
|
254
|
+
* Used by wired Xbox 360/One controllers
|
|
255
|
+
*
|
|
256
|
+
* Button mapping by physical position (Xbox → SNES layout):
|
|
257
|
+
* - Xbox A (bottom) → StandardButton.B
|
|
258
|
+
* - Xbox B (right) → StandardButton.A
|
|
259
|
+
* - Xbox X (left) → StandardButton.Y
|
|
260
|
+
* - Xbox Y (top) → StandardButton.X
|
|
261
|
+
*/
|
|
262
|
+
const xboxWiredProfile: GamepadProfile = {
|
|
263
|
+
name: 'Xbox Wired Controller',
|
|
264
|
+
vendorIds: [VENDOR_MICROSOFT],
|
|
265
|
+
productIds: [], // Match any Microsoft controller
|
|
266
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
267
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
268
|
+
|
|
269
|
+
// Xbox wired controller HID format (19 bytes):
|
|
270
|
+
// Byte 0: Report type (0x20)
|
|
271
|
+
// Byte 1: Unknown (0x00)
|
|
272
|
+
// Byte 2: Packet counter
|
|
273
|
+
// Byte 3: Unknown (0x2c)
|
|
274
|
+
// Byte 4: Start=0x04, Select=0x08, A=0x10, B=0x20, X=0x40, Y=0x80
|
|
275
|
+
// Byte 5: D-pad - Up=0x01, Down=0x02, Left=0x04, Right=0x08
|
|
276
|
+
// Byte 6: LB=0x01, RB=0x02
|
|
277
|
+
// Bytes 7-9: Triggers
|
|
278
|
+
// Bytes 10-17: Analog sticks (16-bit values)
|
|
279
|
+
|
|
280
|
+
if (data.length >= XBOX_WIRED_REPORT_LENGTH && data[0] === XBOX_WIRED_REPORT_TYPE) {
|
|
281
|
+
const buttonsAndMenu = data[XBOX_WIRED_BUTTONS_BYTE];
|
|
282
|
+
const dpad = data[XBOX_WIRED_DPAD_BYTE];
|
|
283
|
+
const shoulders = data[XBOX_WIRED_SHOULDERS_BYTE];
|
|
284
|
+
|
|
285
|
+
// D-pad
|
|
286
|
+
buttons.set(StandardButton.Up, (dpad & DPAD_MASK_UP) !== 0);
|
|
287
|
+
buttons.set(StandardButton.Down, (dpad & DPAD_MASK_DOWN) !== 0);
|
|
288
|
+
buttons.set(StandardButton.Left, (dpad & DPAD_MASK_LEFT) !== 0);
|
|
289
|
+
buttons.set(StandardButton.Right, (dpad & DPAD_MASK_RIGHT) !== 0);
|
|
290
|
+
|
|
291
|
+
// Face buttons mapped by physical position (Xbox → SNES)
|
|
292
|
+
// Xbox A (bottom, 0x10) → SNES B (bottom)
|
|
293
|
+
buttons.set(StandardButton.B, (buttonsAndMenu & XBOX_WIRED_MASK_A) !== 0);
|
|
294
|
+
// Xbox B (right, 0x20) → SNES A (right)
|
|
295
|
+
buttons.set(StandardButton.A, (buttonsAndMenu & XBOX_WIRED_MASK_B) !== 0);
|
|
296
|
+
// Xbox X (left, 0x40) → SNES Y (left)
|
|
297
|
+
buttons.set(StandardButton.Y, (buttonsAndMenu & XBOX_WIRED_MASK_X) !== 0);
|
|
298
|
+
// Xbox Y (top, 0x80) → SNES X (top)
|
|
299
|
+
buttons.set(StandardButton.X, (buttonsAndMenu & XBOX_WIRED_MASK_Y) !== 0);
|
|
300
|
+
|
|
301
|
+
// Shoulder buttons
|
|
302
|
+
buttons.set(StandardButton.L, (shoulders & SHOULDER_MASK_L) !== 0);
|
|
303
|
+
buttons.set(StandardButton.R, (shoulders & SHOULDER_MASK_R) !== 0);
|
|
304
|
+
|
|
305
|
+
// Menu buttons
|
|
306
|
+
buttons.set(StandardButton.Start, (buttonsAndMenu & XBOX_WIRED_MASK_START) !== 0);
|
|
307
|
+
buttons.set(StandardButton.Select, (buttonsAndMenu & XBOX_WIRED_MASK_SELECT) !== 0);
|
|
308
|
+
|
|
309
|
+
// Xbox/Guide button (byte 4, bit 0x01 or 0x02 depending on controller)
|
|
310
|
+
buttons.set(StandardButton.Guide, (buttonsAndMenu & XBOX_WIRED_MASK_GUIDE_1) !== 0 || (buttonsAndMenu & XBOX_WIRED_MASK_GUIDE_2) !== 0);
|
|
311
|
+
|
|
312
|
+
// Also check left analog stick for d-pad (bytes 10-13 are signed 16-bit LE)
|
|
313
|
+
if (data.length >= XBOX_WIRED_LEFT_STICK_Y_OFFSET + 2) {
|
|
314
|
+
applySignedAnalogToDpad(buttons, readInt16LE(data, XBOX_WIRED_LEFT_STICK_X_OFFSET), readInt16LE(data, XBOX_WIRED_LEFT_STICK_Y_OFFSET));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return buttons;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return buttons;
|
|
321
|
+
},
|
|
322
|
+
parseAnalog: (data: Buffer): AnalogState | null => {
|
|
323
|
+
// Xbox wired controller format
|
|
324
|
+
// Bytes 10-11: Left stick X (signed 16-bit LE)
|
|
325
|
+
// Bytes 12-13: Left stick Y (signed 16-bit LE)
|
|
326
|
+
// Bytes 14-15: Right stick X (signed 16-bit LE)
|
|
327
|
+
// Bytes 16-17: Right stick Y (signed 16-bit LE)
|
|
328
|
+
if (data.length >= XBOX_WIRED_REPORT_LENGTH && data[0] === XBOX_WIRED_REPORT_TYPE) {
|
|
329
|
+
const leftX = readInt16LE(data, XBOX_WIRED_LEFT_STICK_X_OFFSET) / ANALOG_INT16_MAX;
|
|
330
|
+
const leftY = readInt16LE(data, XBOX_WIRED_LEFT_STICK_Y_OFFSET) / ANALOG_INT16_MAX;
|
|
331
|
+
const rightX = readInt16LE(data, XBOX_WIRED_RIGHT_STICK_X_OFFSET) / ANALOG_INT16_MAX;
|
|
332
|
+
const rightY = readInt16LE(data, XBOX_WIRED_RIGHT_STICK_Y_OFFSET) / ANALOG_INT16_MAX;
|
|
333
|
+
return { leftX, leftY, rightX, rightY };
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Xbox One / Series controller profile
|
|
341
|
+
* Works on macOS, Windows, and Linux via Bluetooth or USB
|
|
342
|
+
*
|
|
343
|
+
* Button mapping by physical position (Xbox → SNES layout):
|
|
344
|
+
* - Xbox A (bottom) → StandardButton.B
|
|
345
|
+
* - Xbox B (right) → StandardButton.A
|
|
346
|
+
* - Xbox X (left) → StandardButton.Y
|
|
347
|
+
* - Xbox Y (top) → StandardButton.X
|
|
348
|
+
*/
|
|
349
|
+
const xboxOneProfile: GamepadProfile = {
|
|
350
|
+
name: 'Xbox One/Series Controller',
|
|
351
|
+
vendorIds: [
|
|
352
|
+
VENDOR_MICROSOFT,
|
|
353
|
+
VENDOR_PDP,
|
|
354
|
+
VENDOR_HORI,
|
|
355
|
+
VENDOR_RAZER,
|
|
356
|
+
VENDOR_POWERA,
|
|
357
|
+
],
|
|
358
|
+
productIds: [
|
|
359
|
+
// Microsoft Xbox One/Series controllers
|
|
360
|
+
PRODUCT_XBOX_ONE,
|
|
361
|
+
PRODUCT_XBOX_ONE_2015,
|
|
362
|
+
PRODUCT_XBOX_ONE_ELITE,
|
|
363
|
+
PRODUCT_XBOX_ONE_S,
|
|
364
|
+
PRODUCT_XBOX_ONE_S_BT,
|
|
365
|
+
PRODUCT_XBOX_ELITE_S2,
|
|
366
|
+
PRODUCT_XBOX_ELITE_S2_BT,
|
|
367
|
+
PRODUCT_XBOX_SERIES,
|
|
368
|
+
PRODUCT_XBOX_SERIES_BT,
|
|
369
|
+
PRODUCT_XBOX_ADAPTIVE,
|
|
370
|
+
PRODUCT_XBOX_ADAPTIVE_BT,
|
|
371
|
+
PRODUCT_XBOX_ELITE_S2_V2,
|
|
372
|
+
],
|
|
373
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
374
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
375
|
+
|
|
376
|
+
// Xbox Series X|S Bluetooth format (17 bytes):
|
|
377
|
+
// Byte 0: Report ID (0x01)
|
|
378
|
+
// Bytes 1-2: Left stick X (16-bit LE)
|
|
379
|
+
// Bytes 3-4: Left stick Y (16-bit LE)
|
|
380
|
+
// Bytes 5-6: Right stick X (16-bit LE)
|
|
381
|
+
// Bytes 7-8: Right stick Y (16-bit LE)
|
|
382
|
+
// Bytes 9-10: Left trigger (16-bit)
|
|
383
|
+
// Bytes 11-12: Right trigger (16-bit)
|
|
384
|
+
// Byte 13: D-pad hat (0=none, 1=N, 2=NE, 3=E, 4=SE, 5=S, 6=SW, 7=W, 8=NW)
|
|
385
|
+
// Byte 14: Face buttons (A=0x01, B=0x02, X=0x08, Y=0x10)
|
|
386
|
+
// Byte 15: LB=0x01, RB=0x02, View=0x04, Menu=0x08, Xbox=0x10
|
|
387
|
+
|
|
388
|
+
if (data.length >= XBOX_SERIES_BT_REPORT_LENGTH && data[0] === XBOX_SERIES_BT_REPORT_TYPE) {
|
|
389
|
+
// D-pad from hat switch (byte 13)
|
|
390
|
+
const hat = data[XBOX_SERIES_BT_HAT_BYTE];
|
|
391
|
+
const dpad = hatToDpad(hat, true);
|
|
392
|
+
|
|
393
|
+
// Also support left stick as d-pad (bytes 1-4 are 16-bit LE values)
|
|
394
|
+
const leftX = readUint16LE(data, XBOX_SERIES_BT_LEFT_X_OFFSET); // center ~32768
|
|
395
|
+
const leftY = readUint16LE(data, XBOX_SERIES_BT_LEFT_Y_OFFSET);
|
|
396
|
+
const stickDpad = {
|
|
397
|
+
left: leftX < XBOX_SERIES_BT_ANALOG_LOW,
|
|
398
|
+
right: leftX > XBOX_SERIES_BT_ANALOG_HIGH,
|
|
399
|
+
up: leftY < XBOX_SERIES_BT_ANALOG_LOW,
|
|
400
|
+
down: leftY > XBOX_SERIES_BT_ANALOG_HIGH,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
buttons.set(StandardButton.Up, dpad.up || stickDpad.up);
|
|
404
|
+
buttons.set(StandardButton.Down, dpad.down || stickDpad.down);
|
|
405
|
+
buttons.set(StandardButton.Left, dpad.left || stickDpad.left);
|
|
406
|
+
buttons.set(StandardButton.Right, dpad.right || stickDpad.right);
|
|
407
|
+
|
|
408
|
+
// Face buttons mapped by physical position (Xbox → SNES)
|
|
409
|
+
const faceButtons = data[XBOX_SERIES_BT_FACE_BUTTONS_BYTE];
|
|
410
|
+
// Xbox A (bottom, 0x01) → SNES B (bottom)
|
|
411
|
+
buttons.set(StandardButton.B, (faceButtons & XBOX_SERIES_BT_MASK_A) !== 0);
|
|
412
|
+
// Xbox B (right, 0x02) → SNES A (right)
|
|
413
|
+
buttons.set(StandardButton.A, (faceButtons & XBOX_SERIES_BT_MASK_B) !== 0);
|
|
414
|
+
// Xbox X (left, 0x08) → SNES Y (left)
|
|
415
|
+
buttons.set(StandardButton.Y, (faceButtons & XBOX_SERIES_BT_MASK_X) !== 0);
|
|
416
|
+
// Xbox Y (top, 0x10) → SNES X (top)
|
|
417
|
+
buttons.set(StandardButton.X, (faceButtons & XBOX_SERIES_BT_MASK_Y) !== 0);
|
|
418
|
+
|
|
419
|
+
// Shoulder buttons and menu (byte 15)
|
|
420
|
+
const menuButtons = data[XBOX_SERIES_BT_MENU_BUTTONS_BYTE];
|
|
421
|
+
buttons.set(StandardButton.L, (menuButtons & XBOX_SERIES_BT_MASK_LB) !== 0); // LB
|
|
422
|
+
buttons.set(StandardButton.R, (menuButtons & XBOX_SERIES_BT_MASK_RB) !== 0); // RB
|
|
423
|
+
buttons.set(StandardButton.Select, (menuButtons & XBOX_SERIES_BT_MASK_VIEW) !== 0); // View button
|
|
424
|
+
buttons.set(StandardButton.Start, (menuButtons & XBOX_SERIES_BT_MASK_MENU) !== 0); // Menu button
|
|
425
|
+
buttons.set(StandardButton.Guide, (menuButtons & XBOX_SERIES_BT_MASK_XBOX) !== 0); // Xbox button
|
|
426
|
+
|
|
427
|
+
return buttons;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Xbox wired controller format (19 bytes starting with 0x20)
|
|
431
|
+
// Byte 4: Start=0x04, Select=0x08, A=0x10, B=0x20, X=0x40, Y=0x80
|
|
432
|
+
// Byte 5: D-pad - Up=0x01, Down=0x02, Left=0x04, Right=0x08
|
|
433
|
+
// Byte 6: LB=0x01, RB=0x02
|
|
434
|
+
if (data.length >= XBOX_WIRED_REPORT_LENGTH && data[0] === XBOX_WIRED_REPORT_TYPE) {
|
|
435
|
+
const buttonsAndMenu = data[XBOX_WIRED_BUTTONS_BYTE];
|
|
436
|
+
const dpad = data[XBOX_WIRED_DPAD_BYTE];
|
|
437
|
+
const shoulders = data[XBOX_WIRED_SHOULDERS_BYTE];
|
|
438
|
+
|
|
439
|
+
// D-pad
|
|
440
|
+
buttons.set(StandardButton.Up, (dpad & DPAD_MASK_UP) !== 0);
|
|
441
|
+
buttons.set(StandardButton.Down, (dpad & DPAD_MASK_DOWN) !== 0);
|
|
442
|
+
buttons.set(StandardButton.Left, (dpad & DPAD_MASK_LEFT) !== 0);
|
|
443
|
+
buttons.set(StandardButton.Right, (dpad & DPAD_MASK_RIGHT) !== 0);
|
|
444
|
+
|
|
445
|
+
// Face buttons mapped by physical position (Xbox → SNES)
|
|
446
|
+
// Xbox A (bottom, 0x10) → SNES B (bottom)
|
|
447
|
+
buttons.set(StandardButton.B, (buttonsAndMenu & XBOX_WIRED_MASK_A) !== 0);
|
|
448
|
+
// Xbox B (right, 0x20) → SNES A (right)
|
|
449
|
+
buttons.set(StandardButton.A, (buttonsAndMenu & XBOX_WIRED_MASK_B) !== 0);
|
|
450
|
+
// Xbox X (left, 0x40) → SNES Y (left)
|
|
451
|
+
buttons.set(StandardButton.Y, (buttonsAndMenu & XBOX_WIRED_MASK_X) !== 0);
|
|
452
|
+
// Xbox Y (top, 0x80) → SNES X (top)
|
|
453
|
+
buttons.set(StandardButton.X, (buttonsAndMenu & XBOX_WIRED_MASK_Y) !== 0);
|
|
454
|
+
|
|
455
|
+
// Shoulder buttons
|
|
456
|
+
buttons.set(StandardButton.L, (shoulders & SHOULDER_MASK_L) !== 0);
|
|
457
|
+
buttons.set(StandardButton.R, (shoulders & SHOULDER_MASK_R) !== 0);
|
|
458
|
+
|
|
459
|
+
// Menu buttons
|
|
460
|
+
buttons.set(StandardButton.Start, (buttonsAndMenu & XBOX_WIRED_MASK_START) !== 0);
|
|
461
|
+
buttons.set(StandardButton.Select, (buttonsAndMenu & XBOX_WIRED_MASK_SELECT) !== 0);
|
|
462
|
+
|
|
463
|
+
// Xbox/Guide button (byte 4, bit 0x01 or 0x02 depending on controller)
|
|
464
|
+
buttons.set(StandardButton.Guide, (buttonsAndMenu & XBOX_WIRED_MASK_GUIDE_1) !== 0 || (buttonsAndMenu & XBOX_WIRED_MASK_GUIDE_2) !== 0);
|
|
465
|
+
|
|
466
|
+
// Left analog stick (bytes 10-13 are signed 16-bit LE)
|
|
467
|
+
if (data.length >= XBOX_WIRED_LEFT_STICK_Y_OFFSET + 2) {
|
|
468
|
+
applySignedAnalogToDpad(buttons, readInt16LE(data, XBOX_WIRED_LEFT_STICK_X_OFFSET), readInt16LE(data, XBOX_WIRED_LEFT_STICK_Y_OFFSET));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return buttons;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Fallback for other Xbox controller formats or shorter reports
|
|
475
|
+
if (data.length >= PS_MIN_REPORT_LENGTH) {
|
|
476
|
+
// Try generic format - map by physical position
|
|
477
|
+
const btnByte = data[0];
|
|
478
|
+
buttons.set(StandardButton.B, (btnByte & GENERIC_MASK_BUTTON_1) !== 0); // A → B
|
|
479
|
+
buttons.set(StandardButton.A, (btnByte & GENERIC_MASK_BUTTON_2) !== 0); // B → A
|
|
480
|
+
buttons.set(StandardButton.Select, (btnByte & GENERIC_FALLBACK_MASK_SELECT) !== 0);
|
|
481
|
+
buttons.set(StandardButton.Start, (btnByte & GENERIC_FALLBACK_MASK_START) !== 0);
|
|
482
|
+
buttons.set(StandardButton.Up, (btnByte & GENERIC_FALLBACK_MASK_UP) !== 0);
|
|
483
|
+
buttons.set(StandardButton.Down, (btnByte & GENERIC_FALLBACK_MASK_DOWN) !== 0);
|
|
484
|
+
buttons.set(StandardButton.Left, (btnByte & GENERIC_FALLBACK_MASK_LEFT) !== 0);
|
|
485
|
+
buttons.set(StandardButton.Right, (btnByte & GENERIC_FALLBACK_MASK_RIGHT) !== 0);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return buttons;
|
|
489
|
+
},
|
|
490
|
+
parseAnalog: (data: Buffer): AnalogState | null => {
|
|
491
|
+
// Xbox Series X|S Bluetooth format (17 bytes)
|
|
492
|
+
if (data.length >= XBOX_SERIES_BT_REPORT_LENGTH && data[0] === XBOX_SERIES_BT_REPORT_TYPE) {
|
|
493
|
+
// Bytes 1-2: Left stick X (unsigned 16-bit, center at 32768)
|
|
494
|
+
// Bytes 3-4: Left stick Y (unsigned 16-bit, center at 32768)
|
|
495
|
+
// Bytes 5-6: Right stick X
|
|
496
|
+
// Bytes 7-8: Right stick Y
|
|
497
|
+
const leftX = (readUint16LE(data, XBOX_SERIES_BT_LEFT_X_OFFSET) - ANALOG_UINT16_CENTER) / ANALOG_INT16_MAX;
|
|
498
|
+
const leftY = (readUint16LE(data, XBOX_SERIES_BT_LEFT_Y_OFFSET) - ANALOG_UINT16_CENTER) / ANALOG_INT16_MAX;
|
|
499
|
+
const rightX = (readUint16LE(data, XBOX_SERIES_BT_RIGHT_X_OFFSET) - ANALOG_UINT16_CENTER) / ANALOG_INT16_MAX;
|
|
500
|
+
const rightY = (readUint16LE(data, XBOX_SERIES_BT_RIGHT_Y_OFFSET) - ANALOG_UINT16_CENTER) / ANALOG_INT16_MAX;
|
|
501
|
+
return { leftX, leftY, rightX, rightY };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Xbox wired controller format
|
|
505
|
+
if (data.length >= XBOX_WIRED_REPORT_LENGTH && data[0] === XBOX_WIRED_REPORT_TYPE) {
|
|
506
|
+
const leftX = readInt16LE(data, XBOX_WIRED_LEFT_STICK_X_OFFSET) / ANALOG_INT16_MAX;
|
|
507
|
+
const leftY = readInt16LE(data, XBOX_WIRED_LEFT_STICK_Y_OFFSET) / ANALOG_INT16_MAX;
|
|
508
|
+
const rightX = readInt16LE(data, XBOX_WIRED_RIGHT_STICK_X_OFFSET) / ANALOG_INT16_MAX;
|
|
509
|
+
const rightY = readInt16LE(data, XBOX_WIRED_RIGHT_STICK_Y_OFFSET) / ANALOG_INT16_MAX;
|
|
510
|
+
return { leftX, leftY, rightX, rightY };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return null;
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Xbox 360 controller profile
|
|
519
|
+
*
|
|
520
|
+
* Button mapping by physical position (Xbox → SNES layout):
|
|
521
|
+
* - Xbox A (bottom) → StandardButton.B
|
|
522
|
+
* - Xbox B (right) → StandardButton.A
|
|
523
|
+
* - Xbox X (left) → StandardButton.Y
|
|
524
|
+
* - Xbox Y (top) → StandardButton.X
|
|
525
|
+
*/
|
|
526
|
+
const xbox360Profile: GamepadProfile = {
|
|
527
|
+
name: 'Xbox 360 Controller',
|
|
528
|
+
vendorIds: [VENDOR_MICROSOFT],
|
|
529
|
+
productIds: [
|
|
530
|
+
PRODUCT_XBOX_360,
|
|
531
|
+
PRODUCT_XBOX_360_WIRELESS,
|
|
532
|
+
PRODUCT_XBOX_360_WIRELESS_RECEIVER,
|
|
533
|
+
PRODUCT_XBOX_360_WIRELESS_ALT,
|
|
534
|
+
PRODUCT_XBOX_360_WIRELESS_RECEIVER_ALT,
|
|
535
|
+
],
|
|
536
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
537
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
538
|
+
|
|
539
|
+
if (data.length < XBOX_360_MIN_REPORT_LENGTH) {
|
|
540
|
+
return buttons;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Xbox 360 HID format
|
|
544
|
+
// Byte 2: D-pad and menu buttons
|
|
545
|
+
// Byte 3: Face buttons and shoulders
|
|
546
|
+
const btnByte1 = data[XBOX_360_BUTTONS_BYTE_1];
|
|
547
|
+
const btnByte2 = data[XBOX_360_BUTTONS_BYTE_2];
|
|
548
|
+
|
|
549
|
+
// D-pad
|
|
550
|
+
buttons.set(StandardButton.Up, (btnByte1 & XBOX_360_MASK_DPAD_UP) !== 0);
|
|
551
|
+
buttons.set(StandardButton.Down, (btnByte1 & XBOX_360_MASK_DPAD_DOWN) !== 0);
|
|
552
|
+
buttons.set(StandardButton.Left, (btnByte1 & XBOX_360_MASK_DPAD_LEFT) !== 0);
|
|
553
|
+
buttons.set(StandardButton.Right, (btnByte1 & XBOX_360_MASK_DPAD_RIGHT) !== 0);
|
|
554
|
+
|
|
555
|
+
// Start/Back (Select)
|
|
556
|
+
buttons.set(StandardButton.Start, (btnByte1 & XBOX_360_MASK_START) !== 0);
|
|
557
|
+
buttons.set(StandardButton.Select, (btnByte1 & XBOX_360_MASK_BACK) !== 0);
|
|
558
|
+
|
|
559
|
+
// Face buttons mapped by physical position (Xbox → SNES)
|
|
560
|
+
// Xbox A (bottom, 0x10) → SNES B (bottom)
|
|
561
|
+
buttons.set(StandardButton.B, (btnByte2 & XBOX_360_MASK_A) !== 0);
|
|
562
|
+
// Xbox B (right, 0x20) → SNES A (right)
|
|
563
|
+
buttons.set(StandardButton.A, (btnByte2 & XBOX_360_MASK_B) !== 0);
|
|
564
|
+
// Xbox X (left, 0x40) → SNES Y (left)
|
|
565
|
+
buttons.set(StandardButton.Y, (btnByte2 & XBOX_360_MASK_X) !== 0);
|
|
566
|
+
// Xbox Y (top, 0x80) → SNES X (top)
|
|
567
|
+
buttons.set(StandardButton.X, (btnByte2 & XBOX_360_MASK_Y) !== 0);
|
|
568
|
+
|
|
569
|
+
// Shoulder buttons
|
|
570
|
+
buttons.set(StandardButton.L, (btnByte2 & XBOX_360_MASK_LB) !== 0);
|
|
571
|
+
buttons.set(StandardButton.R, (btnByte2 & XBOX_360_MASK_RB) !== 0);
|
|
572
|
+
|
|
573
|
+
return buttons;
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* PlayStation DualShock 4 profile
|
|
579
|
+
*
|
|
580
|
+
* Button mapping by physical position (PlayStation → SNES layout):
|
|
581
|
+
* - Cross (bottom) → StandardButton.B
|
|
582
|
+
* - Circle (right) → StandardButton.A
|
|
583
|
+
* - Square (left) → StandardButton.Y
|
|
584
|
+
* - Triangle (top) → StandardButton.X
|
|
585
|
+
*/
|
|
586
|
+
const dualShock4Profile: GamepadProfile = {
|
|
587
|
+
name: 'PlayStation DualShock 4',
|
|
588
|
+
vendorIds: [VENDOR_SONY],
|
|
589
|
+
productIds: [
|
|
590
|
+
PRODUCT_DUALSHOCK4_V1,
|
|
591
|
+
PRODUCT_DUALSHOCK4_V2,
|
|
592
|
+
PRODUCT_DUALSHOCK4_ADAPTER,
|
|
593
|
+
],
|
|
594
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
595
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
596
|
+
|
|
597
|
+
if (data.length < PS_MIN_REPORT_LENGTH) {
|
|
598
|
+
return buttons;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// DS4 HID report format (USB):
|
|
602
|
+
// Byte 0: Report ID (0x01)
|
|
603
|
+
// Byte 1: Left stick X
|
|
604
|
+
// Byte 2: Left stick Y
|
|
605
|
+
// Byte 3: Right stick X
|
|
606
|
+
// Byte 4: Right stick Y
|
|
607
|
+
// Byte 5: Hat switch (lower 4 bits) + face buttons
|
|
608
|
+
// Byte 6-7: Shoulders and menu buttons
|
|
609
|
+
|
|
610
|
+
const offset = data[0] === PS_REPORT_ID ? 0 : -1; // Adjust for report ID
|
|
611
|
+
|
|
612
|
+
const leftX = data[DS4_LEFT_X_OFFSET + offset] ?? PS_ANALOG_CENTER;
|
|
613
|
+
const leftY = data[DS4_LEFT_Y_OFFSET + offset] ?? PS_ANALOG_CENTER;
|
|
614
|
+
const hatAndButtons = data[DS4_HAT_AND_BUTTONS_OFFSET + offset] ?? 0;
|
|
615
|
+
const btnByte1 = data[DS4_SHOULDERS_OFFSET + offset] ?? 0;
|
|
616
|
+
|
|
617
|
+
// Hat switch for d-pad (lower 4 bits)
|
|
618
|
+
const hat = hatAndButtons & DPAD_HAT_MASK;
|
|
619
|
+
const dpad = hatToDpad(hat);
|
|
620
|
+
|
|
621
|
+
// Also check left stick
|
|
622
|
+
const stickDpad = analogToDpad(leftX, leftY);
|
|
623
|
+
|
|
624
|
+
buttons.set(StandardButton.Up, dpad.up || stickDpad.up);
|
|
625
|
+
buttons.set(StandardButton.Down, dpad.down || stickDpad.down);
|
|
626
|
+
buttons.set(StandardButton.Left, dpad.left || stickDpad.left);
|
|
627
|
+
buttons.set(StandardButton.Right, dpad.right || stickDpad.right);
|
|
628
|
+
|
|
629
|
+
// Face buttons mapped by physical position (PlayStation → SNES)
|
|
630
|
+
// Cross (bottom, 0x20) → SNES B (bottom)
|
|
631
|
+
buttons.set(StandardButton.B, (hatAndButtons & DS4_MASK_CROSS) !== 0);
|
|
632
|
+
// Circle (right, 0x40) → SNES A (right)
|
|
633
|
+
buttons.set(StandardButton.A, (hatAndButtons & DS4_MASK_CIRCLE) !== 0);
|
|
634
|
+
// Square (left, 0x10) → SNES Y (left)
|
|
635
|
+
buttons.set(StandardButton.Y, (hatAndButtons & DS4_MASK_SQUARE) !== 0);
|
|
636
|
+
// Triangle (top, 0x80) → SNES X (top)
|
|
637
|
+
buttons.set(StandardButton.X, (hatAndButtons & DS4_MASK_TRIANGLE) !== 0);
|
|
638
|
+
|
|
639
|
+
// Shoulder buttons
|
|
640
|
+
buttons.set(StandardButton.L, (btnByte1 & SHOULDER_MASK_L) !== 0); // L1
|
|
641
|
+
buttons.set(StandardButton.R, (btnByte1 & SHOULDER_MASK_R) !== 0); // R1
|
|
642
|
+
buttons.set(StandardButton.L2, (btnByte1 & SHOULDER_MASK_L2) !== 0); // L2
|
|
643
|
+
buttons.set(StandardButton.R2, (btnByte1 & SHOULDER_MASK_R2) !== 0); // R2
|
|
644
|
+
|
|
645
|
+
// Share = Select, Options = Start
|
|
646
|
+
buttons.set(StandardButton.Select, (btnByte1 & GENERIC_FALLBACK_MASK_UP) !== 0);
|
|
647
|
+
buttons.set(StandardButton.Start, (btnByte1 & GENERIC_FALLBACK_MASK_DOWN) !== 0);
|
|
648
|
+
|
|
649
|
+
// PS button (byte 7, bit 0x01)
|
|
650
|
+
if (data.length >= PS_MIN_REPORT_LENGTH) {
|
|
651
|
+
const btnByte2 = data[DS4_PS_BUTTON_OFFSET + offset] ?? 0;
|
|
652
|
+
buttons.set(StandardButton.Guide, (btnByte2 & DUALSENSE_PS_BUTTON_MASK) !== 0);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return buttons;
|
|
656
|
+
},
|
|
657
|
+
parseAnalog: (data: Buffer): AnalogState | null => {
|
|
658
|
+
if (data.length < PS_MIN_REPORT_LENGTH) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const offset = data[0] === PS_REPORT_ID ? 0 : -1;
|
|
663
|
+
|
|
664
|
+
// DS4 sticks are unsigned 8-bit (0-255, center at 128)
|
|
665
|
+
const leftX = ((data[DS4_LEFT_X_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
666
|
+
const leftY = ((data[DS4_LEFT_Y_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
667
|
+
const rightX = ((data[DS4_RIGHT_X_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
668
|
+
const rightY = ((data[DS4_RIGHT_Y_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
669
|
+
|
|
670
|
+
return { leftX, leftY, rightX, rightY };
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* PlayStation DualSense profile
|
|
676
|
+
*
|
|
677
|
+
* Button mapping by physical position (PlayStation → SNES layout):
|
|
678
|
+
* - Cross (bottom) → StandardButton.B
|
|
679
|
+
* - Circle (right) → StandardButton.A
|
|
680
|
+
* - Square (left) → StandardButton.Y
|
|
681
|
+
* - Triangle (top) → StandardButton.X
|
|
682
|
+
*/
|
|
683
|
+
const dualSenseProfile: GamepadProfile = {
|
|
684
|
+
name: 'PlayStation DualSense',
|
|
685
|
+
vendorIds: [VENDOR_SONY],
|
|
686
|
+
productIds: [
|
|
687
|
+
PRODUCT_DUALSENSE,
|
|
688
|
+
PRODUCT_DUALSENSE_EDGE,
|
|
689
|
+
],
|
|
690
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
691
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
692
|
+
|
|
693
|
+
if (data.length < PS_MIN_REPORT_LENGTH) {
|
|
694
|
+
return buttons;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// DualSense USB HID format is similar to DS4
|
|
698
|
+
// Byte 0: Report ID (0x01)
|
|
699
|
+
// Byte 1: Left stick X
|
|
700
|
+
// Byte 2: Left stick Y
|
|
701
|
+
// Byte 3: Right stick X
|
|
702
|
+
// Byte 4: Right stick Y
|
|
703
|
+
// Byte 5: Triggers (L2)
|
|
704
|
+
// Byte 6: Triggers (R2)
|
|
705
|
+
// Byte 7: Hat switch + face buttons
|
|
706
|
+
// Byte 8+: Shoulders and menu buttons
|
|
707
|
+
|
|
708
|
+
const offset = data[0] === PS_REPORT_ID ? 0 : -1;
|
|
709
|
+
|
|
710
|
+
const leftX = data[DUALSENSE_LEFT_X_OFFSET + offset] ?? PS_ANALOG_CENTER;
|
|
711
|
+
const leftY = data[DUALSENSE_LEFT_Y_OFFSET + offset] ?? PS_ANALOG_CENTER;
|
|
712
|
+
const hatAndButtons = data[DUALSENSE_HAT_AND_BUTTONS_OFFSET + offset] ?? 0;
|
|
713
|
+
const btnByte1 = data[DUALSENSE_SHOULDERS_OFFSET + offset] ?? 0;
|
|
714
|
+
|
|
715
|
+
// Hat switch (lower 4 bits)
|
|
716
|
+
const hat = hatAndButtons & DPAD_HAT_MASK;
|
|
717
|
+
const dpad = hatToDpad(hat);
|
|
718
|
+
const stickDpad = analogToDpad(leftX, leftY);
|
|
719
|
+
|
|
720
|
+
buttons.set(StandardButton.Up, dpad.up || stickDpad.up);
|
|
721
|
+
buttons.set(StandardButton.Down, dpad.down || stickDpad.down);
|
|
722
|
+
buttons.set(StandardButton.Left, dpad.left || stickDpad.left);
|
|
723
|
+
buttons.set(StandardButton.Right, dpad.right || stickDpad.right);
|
|
724
|
+
|
|
725
|
+
// Face buttons mapped by physical position (PlayStation → SNES)
|
|
726
|
+
// Cross (bottom, 0x20) → SNES B (bottom)
|
|
727
|
+
buttons.set(StandardButton.B, (hatAndButtons & DS4_MASK_CROSS) !== 0);
|
|
728
|
+
// Circle (right, 0x40) → SNES A (right)
|
|
729
|
+
buttons.set(StandardButton.A, (hatAndButtons & DS4_MASK_CIRCLE) !== 0);
|
|
730
|
+
// Square (left, 0x10) → SNES Y (left)
|
|
731
|
+
buttons.set(StandardButton.Y, (hatAndButtons & DS4_MASK_SQUARE) !== 0);
|
|
732
|
+
// Triangle (top, 0x80) → SNES X (top)
|
|
733
|
+
buttons.set(StandardButton.X, (hatAndButtons & DS4_MASK_TRIANGLE) !== 0);
|
|
734
|
+
|
|
735
|
+
// Shoulder buttons
|
|
736
|
+
buttons.set(StandardButton.L, (btnByte1 & SHOULDER_MASK_L) !== 0); // L1
|
|
737
|
+
buttons.set(StandardButton.R, (btnByte1 & SHOULDER_MASK_R) !== 0); // R1
|
|
738
|
+
buttons.set(StandardButton.L2, (btnByte1 & SHOULDER_MASK_L2) !== 0); // L2
|
|
739
|
+
buttons.set(StandardButton.R2, (btnByte1 & SHOULDER_MASK_R2) !== 0); // R2
|
|
740
|
+
|
|
741
|
+
// Create = Select, Options = Start
|
|
742
|
+
buttons.set(StandardButton.Select, (btnByte1 & GENERIC_FALLBACK_MASK_UP) !== 0);
|
|
743
|
+
buttons.set(StandardButton.Start, (btnByte1 & GENERIC_FALLBACK_MASK_DOWN) !== 0);
|
|
744
|
+
|
|
745
|
+
// PS button (byte 9, bit 0x01)
|
|
746
|
+
if (data.length >= DUALSENSE_PS_BUTTON_MIN_LENGTH) {
|
|
747
|
+
const btnByte2 = data[DUALSENSE_PS_BUTTON_OFFSET + offset] ?? 0;
|
|
748
|
+
buttons.set(StandardButton.Guide, (btnByte2 & DUALSENSE_PS_BUTTON_MASK) !== 0);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return buttons;
|
|
752
|
+
},
|
|
753
|
+
parseAnalog: (data: Buffer): AnalogState | null => {
|
|
754
|
+
if (data.length < PS_MIN_REPORT_LENGTH) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const offset = data[0] === PS_REPORT_ID ? 0 : -1;
|
|
759
|
+
|
|
760
|
+
// DualSense sticks are unsigned 8-bit (0-255, center at 128)
|
|
761
|
+
const leftX = ((data[DUALSENSE_LEFT_X_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
762
|
+
const leftY = ((data[DUALSENSE_LEFT_Y_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
763
|
+
const rightX = ((data[DUALSENSE_RIGHT_X_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
764
|
+
const rightY = ((data[DUALSENSE_RIGHT_Y_OFFSET + offset] ?? PS_ANALOG_CENTER) - PS_ANALOG_CENTER) / PS_ANALOG_RANGE;
|
|
765
|
+
|
|
766
|
+
return { leftX, leftY, rightX, rightY };
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Nintendo Switch Pro Controller profile
|
|
772
|
+
*
|
|
773
|
+
* Nintendo uses the same physical button layout as SNES:
|
|
774
|
+
* - B (bottom) → StandardButton.B
|
|
775
|
+
* - A (right) → StandardButton.A
|
|
776
|
+
* - Y (left) → StandardButton.Y
|
|
777
|
+
* - X (top) → StandardButton.X
|
|
778
|
+
*/
|
|
779
|
+
const switchProProfile: GamepadProfile = {
|
|
780
|
+
name: 'Nintendo Switch Pro Controller',
|
|
781
|
+
vendorIds: [VENDOR_NINTENDO],
|
|
782
|
+
productIds: [
|
|
783
|
+
PRODUCT_SWITCH_PRO,
|
|
784
|
+
PRODUCT_SNES_CONTROLLER,
|
|
785
|
+
PRODUCT_N64_CONTROLLER,
|
|
786
|
+
PRODUCT_GENESIS_CONTROLLER,
|
|
787
|
+
],
|
|
788
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
789
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
790
|
+
|
|
791
|
+
if (data.length < SWITCH_PRO_MIN_REPORT_LENGTH) {
|
|
792
|
+
return buttons;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Switch Pro Controller USB HID format
|
|
796
|
+
// Various report formats depending on mode
|
|
797
|
+
// Standard HID mode uses different format than Switch mode
|
|
798
|
+
|
|
799
|
+
const btnByte1 = data[SWITCH_PRO_BUTTONS_BYTE_1];
|
|
800
|
+
const btnByte2 = data[SWITCH_PRO_BUTTONS_BYTE_2];
|
|
801
|
+
const btnByte3 = data[SWITCH_PRO_BUTTONS_BYTE_3];
|
|
802
|
+
|
|
803
|
+
// D-pad from byte 3 (hat-style in standard HID)
|
|
804
|
+
const hat = btnByte3 & DPAD_HAT_MASK;
|
|
805
|
+
const dpad = hatToDpad(hat);
|
|
806
|
+
|
|
807
|
+
buttons.set(StandardButton.Up, dpad.up);
|
|
808
|
+
buttons.set(StandardButton.Down, dpad.down);
|
|
809
|
+
buttons.set(StandardButton.Left, dpad.left);
|
|
810
|
+
buttons.set(StandardButton.Right, dpad.right);
|
|
811
|
+
|
|
812
|
+
// Face buttons - Nintendo layout matches SNES directly
|
|
813
|
+
// B (bottom, 0x01) → SNES B (bottom)
|
|
814
|
+
buttons.set(StandardButton.B, (btnByte1 & SWITCH_PRO_MASK_B) !== 0);
|
|
815
|
+
// A (right, 0x04) → SNES A (right)
|
|
816
|
+
buttons.set(StandardButton.A, (btnByte1 & SWITCH_PRO_MASK_A) !== 0);
|
|
817
|
+
// Y (left, 0x02) → SNES Y (left)
|
|
818
|
+
buttons.set(StandardButton.Y, (btnByte1 & SWITCH_PRO_MASK_Y) !== 0);
|
|
819
|
+
// X (top, 0x08) → SNES X (top)
|
|
820
|
+
buttons.set(StandardButton.X, (btnByte1 & SWITCH_PRO_MASK_X) !== 0);
|
|
821
|
+
|
|
822
|
+
// Shoulder buttons
|
|
823
|
+
buttons.set(StandardButton.L, (btnByte1 & SWITCH_PRO_MASK_L) !== 0); // L
|
|
824
|
+
buttons.set(StandardButton.R, (btnByte1 & SWITCH_PRO_MASK_R) !== 0); // R
|
|
825
|
+
buttons.set(StandardButton.L2, (btnByte1 & SWITCH_PRO_MASK_ZL) !== 0); // ZL
|
|
826
|
+
buttons.set(StandardButton.R2, (btnByte1 & SWITCH_PRO_MASK_ZR) !== 0); // ZR
|
|
827
|
+
|
|
828
|
+
// +/- buttons
|
|
829
|
+
buttons.set(StandardButton.Start, (btnByte2 & SWITCH_PRO_MASK_PLUS) !== 0); // +
|
|
830
|
+
buttons.set(StandardButton.Select, (btnByte2 & SWITCH_PRO_MASK_MINUS) !== 0); // -
|
|
831
|
+
|
|
832
|
+
// Home button (byte 2, bit 0x10)
|
|
833
|
+
buttons.set(StandardButton.Guide, (btnByte2 & SWITCH_PRO_MASK_HOME) !== 0);
|
|
834
|
+
|
|
835
|
+
return buttons;
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* 8BitDo controller profile (various retro-style controllers)
|
|
841
|
+
*
|
|
842
|
+
* 8BitDo controllers typically use SNES-style layout
|
|
843
|
+
*/
|
|
844
|
+
const eightBitDoProfile: GamepadProfile = {
|
|
845
|
+
name: '8BitDo Controller',
|
|
846
|
+
vendorIds: [
|
|
847
|
+
VENDOR_8BITDO,
|
|
848
|
+
VENDOR_MICROSOFT, // 8BitDo in Xbox mode reports as Microsoft
|
|
849
|
+
],
|
|
850
|
+
productIds: [
|
|
851
|
+
PRODUCT_8BITDO_SN30,
|
|
852
|
+
PRODUCT_8BITDO_SN30_PRO,
|
|
853
|
+
PRODUCT_8BITDO_SN30_PRO_PLUS,
|
|
854
|
+
PRODUCT_8BITDO_PRO_2,
|
|
855
|
+
PRODUCT_8BITDO_ULTIMATE,
|
|
856
|
+
PRODUCT_8BITDO_SF30,
|
|
857
|
+
PRODUCT_8BITDO_SF30_PRO,
|
|
858
|
+
],
|
|
859
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
860
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
861
|
+
|
|
862
|
+
if (data.length < EIGHTBITDO_MIN_REPORT_LENGTH) {
|
|
863
|
+
return buttons;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// 8BitDo controllers in D-input mode
|
|
867
|
+
// Format varies by model, this handles common format
|
|
868
|
+
const leftX = data[EIGHTBITDO_LEFT_X_OFFSET];
|
|
869
|
+
const leftY = data[EIGHTBITDO_LEFT_Y_OFFSET];
|
|
870
|
+
const btnByte1 = data[EIGHTBITDO_BUTTONS_BYTE_1];
|
|
871
|
+
const btnByte2 = data[EIGHTBITDO_BUTTONS_BYTE_2];
|
|
872
|
+
|
|
873
|
+
// D-pad from hat or analog stick
|
|
874
|
+
const stickDpad = analogToDpad(leftX, leftY);
|
|
875
|
+
buttons.set(StandardButton.Up, stickDpad.up || (btnByte1 & EIGHTBITDO_MASK_DPAD_UP) !== 0);
|
|
876
|
+
buttons.set(StandardButton.Down, stickDpad.down || (btnByte1 & EIGHTBITDO_MASK_DPAD_DOWN) !== 0);
|
|
877
|
+
buttons.set(StandardButton.Left, stickDpad.left || (btnByte1 & EIGHTBITDO_MASK_DPAD_LEFT) !== 0);
|
|
878
|
+
buttons.set(StandardButton.Right, stickDpad.right || (btnByte1 & EIGHTBITDO_MASK_DPAD_RIGHT) !== 0);
|
|
879
|
+
|
|
880
|
+
// Face buttons - 8BitDo uses SNES layout
|
|
881
|
+
// B (bottom, 0x01) → StandardButton.B
|
|
882
|
+
buttons.set(StandardButton.B, (btnByte2 & EIGHTBITDO_MASK_B) !== 0);
|
|
883
|
+
// A (right, 0x02) → StandardButton.A
|
|
884
|
+
buttons.set(StandardButton.A, (btnByte2 & EIGHTBITDO_MASK_A) !== 0);
|
|
885
|
+
// Y (left, 0x04) → StandardButton.Y
|
|
886
|
+
buttons.set(StandardButton.Y, (btnByte2 & EIGHTBITDO_MASK_Y) !== 0);
|
|
887
|
+
// X (top, 0x08) → StandardButton.X
|
|
888
|
+
buttons.set(StandardButton.X, (btnByte2 & EIGHTBITDO_MASK_X) !== 0);
|
|
889
|
+
|
|
890
|
+
// Shoulder buttons
|
|
891
|
+
buttons.set(StandardButton.L, (btnByte1 & EIGHTBITDO_MASK_L) !== 0);
|
|
892
|
+
buttons.set(StandardButton.R, (btnByte1 & EIGHTBITDO_MASK_R) !== 0);
|
|
893
|
+
|
|
894
|
+
// Start/Select
|
|
895
|
+
buttons.set(StandardButton.Start, (btnByte2 & EIGHTBITDO_MASK_START) !== 0);
|
|
896
|
+
buttons.set(StandardButton.Select, (btnByte2 & EIGHTBITDO_MASK_SELECT) !== 0);
|
|
897
|
+
|
|
898
|
+
return buttons;
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Generic USB Gamepad profile
|
|
904
|
+
* Fallback for unknown controllers - tries common HID formats
|
|
905
|
+
* Uses physical position mapping (Xbox-style: bottom=B, right=A, left=Y, top=X)
|
|
906
|
+
*/
|
|
907
|
+
const genericGamepadProfile: GamepadProfile = {
|
|
908
|
+
name: 'Generic Gamepad',
|
|
909
|
+
vendorIds: [], // Match any vendor
|
|
910
|
+
productIds: [], // Match any product
|
|
911
|
+
parseReport: (data: Buffer): Map<StandardButton, boolean> => {
|
|
912
|
+
const buttons = new Map<StandardButton, boolean>();
|
|
913
|
+
|
|
914
|
+
if (data.length < GENERIC_MIN_REPORT_LENGTH) {
|
|
915
|
+
return buttons;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Format A: Xbox-style wired controller (19 bytes starting with 0x20)
|
|
919
|
+
// Byte 4: Start=0x04, Select=0x08, A=0x10, B=0x20, X=0x40, Y=0x80
|
|
920
|
+
// Byte 5: D-pad - Up=0x01, Down=0x02, Left=0x04, Right=0x08
|
|
921
|
+
// Byte 6: LB=0x01, RB=0x02
|
|
922
|
+
if (data.length >= XBOX_WIRED_REPORT_LENGTH && data[0] === XBOX_WIRED_REPORT_TYPE) {
|
|
923
|
+
const buttonsAndMenu = data[XBOX_WIRED_BUTTONS_BYTE];
|
|
924
|
+
const dpad = data[XBOX_WIRED_DPAD_BYTE];
|
|
925
|
+
const shoulders = data[XBOX_WIRED_SHOULDERS_BYTE];
|
|
926
|
+
|
|
927
|
+
// D-pad
|
|
928
|
+
buttons.set(StandardButton.Up, (dpad & DPAD_MASK_UP) !== 0);
|
|
929
|
+
buttons.set(StandardButton.Down, (dpad & DPAD_MASK_DOWN) !== 0);
|
|
930
|
+
buttons.set(StandardButton.Left, (dpad & DPAD_MASK_LEFT) !== 0);
|
|
931
|
+
buttons.set(StandardButton.Right, (dpad & DPAD_MASK_RIGHT) !== 0);
|
|
932
|
+
|
|
933
|
+
// Face buttons mapped by physical position (Xbox → SNES)
|
|
934
|
+
buttons.set(StandardButton.B, (buttonsAndMenu & XBOX_WIRED_MASK_A) !== 0); // A (bottom)
|
|
935
|
+
buttons.set(StandardButton.A, (buttonsAndMenu & XBOX_WIRED_MASK_B) !== 0); // B (right)
|
|
936
|
+
buttons.set(StandardButton.Y, (buttonsAndMenu & XBOX_WIRED_MASK_X) !== 0); // X (left)
|
|
937
|
+
buttons.set(StandardButton.X, (buttonsAndMenu & XBOX_WIRED_MASK_Y) !== 0); // Y (top)
|
|
938
|
+
|
|
939
|
+
// Shoulder buttons
|
|
940
|
+
buttons.set(StandardButton.L, (shoulders & SHOULDER_MASK_L) !== 0);
|
|
941
|
+
buttons.set(StandardButton.R, (shoulders & SHOULDER_MASK_R) !== 0);
|
|
942
|
+
|
|
943
|
+
// Menu buttons
|
|
944
|
+
buttons.set(StandardButton.Start, (buttonsAndMenu & XBOX_WIRED_MASK_START) !== 0);
|
|
945
|
+
buttons.set(StandardButton.Select, (buttonsAndMenu & XBOX_WIRED_MASK_SELECT) !== 0);
|
|
946
|
+
|
|
947
|
+
// Also check left analog stick for d-pad (bytes 10-13 are signed 16-bit LE)
|
|
948
|
+
if (data.length >= XBOX_WIRED_LEFT_STICK_Y_OFFSET + 2) {
|
|
949
|
+
applySignedAnalogToDpad(buttons, readInt16LE(data, XBOX_WIRED_LEFT_STICK_X_OFFSET), readInt16LE(data, XBOX_WIRED_LEFT_STICK_Y_OFFSET));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return buttons;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Try to detect common generic gamepad formats
|
|
956
|
+
// Most cheap USB gamepads follow similar patterns
|
|
957
|
+
|
|
958
|
+
// Format 1: Analog sticks in first 4 bytes, buttons after
|
|
959
|
+
if (data.length >= GENERIC_FORMAT_A_MIN_LENGTH) {
|
|
960
|
+
const leftX = data[GENERIC_ANALOG_OFFSET_X];
|
|
961
|
+
const leftY = data[GENERIC_ANALOG_OFFSET_Y];
|
|
962
|
+
|
|
963
|
+
const stickDpad = analogToDpad(leftX, leftY);
|
|
964
|
+
|
|
965
|
+
// Many generic gamepads also have hat switch in a button byte
|
|
966
|
+
const possibleHat = data[GENERIC_HAT_BYTE];
|
|
967
|
+
const hatDpad = possibleHat <= GENERIC_MAX_HAT_VALUE ? hatToDpad(possibleHat) : { up: false, down: false, left: false, right: false };
|
|
968
|
+
|
|
969
|
+
buttons.set(StandardButton.Up, stickDpad.up || hatDpad.up);
|
|
970
|
+
buttons.set(StandardButton.Down, stickDpad.down || hatDpad.down);
|
|
971
|
+
buttons.set(StandardButton.Left, stickDpad.left || hatDpad.left);
|
|
972
|
+
buttons.set(StandardButton.Right, stickDpad.right || hatDpad.right);
|
|
973
|
+
|
|
974
|
+
// Buttons typically in bytes 5-6 for this format
|
|
975
|
+
const btnByte1 = data[GENERIC_BUTTONS_BYTE_1];
|
|
976
|
+
const btnByte2 = data[GENERIC_BUTTONS_BYTE_2];
|
|
977
|
+
|
|
978
|
+
// Face buttons - output individually
|
|
979
|
+
buttons.set(StandardButton.B, (btnByte1 & GENERIC_MASK_BUTTON_1) !== 0); // Button 1 (bottom)
|
|
980
|
+
buttons.set(StandardButton.A, (btnByte1 & GENERIC_MASK_BUTTON_2) !== 0); // Button 2 (right)
|
|
981
|
+
buttons.set(StandardButton.Y, (btnByte1 & GENERIC_MASK_BUTTON_3) !== 0); // Button 3 (left)
|
|
982
|
+
buttons.set(StandardButton.X, (btnByte1 & GENERIC_MASK_BUTTON_4) !== 0); // Button 4 (top)
|
|
983
|
+
buttons.set(StandardButton.L, (btnByte1 & GENERIC_MASK_BUTTON_L) !== 0); // L shoulder
|
|
984
|
+
buttons.set(StandardButton.R, (btnByte1 & GENERIC_MASK_BUTTON_R) !== 0); // R shoulder
|
|
985
|
+
buttons.set(StandardButton.Select, (btnByte1 & GENERIC_MASK_BUTTON_SELECT) !== 0 || (btnByte2 & GENERIC_MASK_BUTTON_1) !== 0);
|
|
986
|
+
buttons.set(StandardButton.Start, (btnByte1 & GENERIC_MASK_BUTTON_START) !== 0 || (btnByte2 & GENERIC_MASK_BUTTON_2) !== 0);
|
|
987
|
+
} else {
|
|
988
|
+
// Format 2: Very simple - everything in 1-2 bytes
|
|
989
|
+
const btnByte = data[0];
|
|
990
|
+
|
|
991
|
+
buttons.set(StandardButton.B, (btnByte & GENERIC_MASK_BUTTON_1) !== 0); // Button 1
|
|
992
|
+
buttons.set(StandardButton.A, (btnByte & GENERIC_MASK_BUTTON_2) !== 0); // Button 2
|
|
993
|
+
buttons.set(StandardButton.Select, (btnByte & GENERIC_FALLBACK_MASK_SELECT) !== 0);
|
|
994
|
+
buttons.set(StandardButton.Start, (btnByte & GENERIC_FALLBACK_MASK_START) !== 0);
|
|
995
|
+
buttons.set(StandardButton.Up, (btnByte & GENERIC_FALLBACK_MASK_UP) !== 0);
|
|
996
|
+
buttons.set(StandardButton.Down, (btnByte & GENERIC_FALLBACK_MASK_DOWN) !== 0);
|
|
997
|
+
buttons.set(StandardButton.Left, (btnByte & GENERIC_FALLBACK_MASK_LEFT) !== 0);
|
|
998
|
+
buttons.set(StandardButton.Right, (btnByte & GENERIC_FALLBACK_MASK_RIGHT) !== 0);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return buttons;
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* All known gamepad profiles, ordered by specificity
|
|
1007
|
+
* More specific profiles should come first
|
|
1008
|
+
*/
|
|
1009
|
+
export const gamepadProfiles: GamepadProfile[] = [
|
|
1010
|
+
xboxWiredProfile,
|
|
1011
|
+
xboxOneProfile,
|
|
1012
|
+
xbox360Profile,
|
|
1013
|
+
dualShock4Profile,
|
|
1014
|
+
dualSenseProfile,
|
|
1015
|
+
switchProProfile,
|
|
1016
|
+
eightBitDoProfile,
|
|
1017
|
+
genericGamepadProfile, // Fallback - must be last
|
|
1018
|
+
];
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Find the best matching profile for a device
|
|
1022
|
+
*/
|
|
1023
|
+
export const findProfile = (vendorId: number, productId: number): GamepadProfile => {
|
|
1024
|
+
// First try to find exact vendor + product match
|
|
1025
|
+
for (const profile of gamepadProfiles) {
|
|
1026
|
+
if (profile.vendorIds.length === 0) {continue;} // Skip generic fallback
|
|
1027
|
+
if (!profile.vendorIds.includes(vendorId)) {continue;}
|
|
1028
|
+
if (profile.productIds.length > 0 && profile.productIds.includes(productId)) {
|
|
1029
|
+
return profile;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Then try vendor-only match
|
|
1034
|
+
for (const profile of gamepadProfiles) {
|
|
1035
|
+
if (profile.vendorIds.length === 0) {continue;}
|
|
1036
|
+
if (profile.vendorIds.includes(vendorId) && profile.productIds.length === 0) {
|
|
1037
|
+
return profile;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Fall back to generic profile
|
|
1042
|
+
return genericGamepadProfile;
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Check if a device looks like a gamepad based on HID usage
|
|
1047
|
+
*/
|
|
1048
|
+
export const isGamepadDevice = (device: {
|
|
1049
|
+
vendorId?: number;
|
|
1050
|
+
productId?: number;
|
|
1051
|
+
usagePage?: number;
|
|
1052
|
+
usage?: number;
|
|
1053
|
+
product?: string;
|
|
1054
|
+
}): boolean => {
|
|
1055
|
+
// HID usage page 0x01 = Generic Desktop Controls
|
|
1056
|
+
// Usage 0x04 = Joystick, 0x05 = Gamepad
|
|
1057
|
+
if (device.usagePage === HID_USAGE_PAGE_GENERIC_DESKTOP && (device.usage === HID_USAGE_JOYSTICK || device.usage === HID_USAGE_GAMEPAD)) {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check for known gaming vendor IDs
|
|
1062
|
+
const gamingVendors = [
|
|
1063
|
+
VENDOR_MICROSOFT,
|
|
1064
|
+
VENDOR_SONY,
|
|
1065
|
+
VENDOR_NINTENDO,
|
|
1066
|
+
VENDOR_8BITDO,
|
|
1067
|
+
VENDOR_PDP,
|
|
1068
|
+
VENDOR_HORI,
|
|
1069
|
+
VENDOR_RAZER,
|
|
1070
|
+
VENDOR_POWERA,
|
|
1071
|
+
VENDOR_VALVE,
|
|
1072
|
+
VENDOR_LOGITECH,
|
|
1073
|
+
VENDOR_DRAGONRISE,
|
|
1074
|
+
VENDOR_GENERIC_0810,
|
|
1075
|
+
VENDOR_HONEYBEE,
|
|
1076
|
+
VENDOR_GENERIC_1A34,
|
|
1077
|
+
VENDOR_POWERA_BDA,
|
|
1078
|
+
];
|
|
1079
|
+
|
|
1080
|
+
if (device.vendorId && gamingVendors.includes(device.vendorId)) {
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Check product name for gamepad-related keywords
|
|
1085
|
+
const productName = device.product?.toLowerCase() ?? '';
|
|
1086
|
+
const gamepadKeywords = [
|
|
1087
|
+
'gamepad',
|
|
1088
|
+
'controller',
|
|
1089
|
+
'joystick',
|
|
1090
|
+
'xbox',
|
|
1091
|
+
'playstation',
|
|
1092
|
+
'dualshock',
|
|
1093
|
+
'dualsense',
|
|
1094
|
+
'joycon',
|
|
1095
|
+
'switch',
|
|
1096
|
+
'pro controller',
|
|
1097
|
+
];
|
|
1098
|
+
|
|
1099
|
+
return gamepadKeywords.some((keyword) => productName.includes(keyword));
|
|
1100
|
+
};
|