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.
Files changed (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. 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
+ };