@nearcade/virtual-gamepad 1.0.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.
@@ -0,0 +1,544 @@
1
+ """
2
+ Windows ViGEmBus backend
3
+ Virtual controller + KBM injection via vgamepad and pyautogui.
4
+
5
+ All errors are emitted as JSON to stdout so the Node.js orchestrator
6
+ (InputOrchestrator.js) can surface them to the Electron frontend:
7
+ {"type": "error", "message": "...", "code": "..."}
8
+ {"type": "ready", "message": "..."}
9
+ {"type": "log", "message": "..."}
10
+ """
11
+
12
+ import sys
13
+ import json
14
+ import gc
15
+ import socket
16
+ import struct
17
+ import queue
18
+ import threading
19
+
20
+
21
+ # ── JSON protocol helpers ─────────────────────────────────────────────────────
22
+
23
+ def _emit(payload: dict):
24
+ """Write a JSON line to stdout, flushed immediately."""
25
+ print(json.dumps(payload), flush=True)
26
+
27
+ def _log(msg: str):
28
+ _emit({"type": "log", "message": msg})
29
+
30
+ def _error(msg: str, code: str = "VIGEM_ERROR"):
31
+ _emit({"type": "error", "message": msg, "code": code})
32
+
33
+
34
+ # ── Dependency checks — emit structured JSON on failure ──────────────────────
35
+
36
+ try:
37
+ import vgamepad as vg
38
+ except ImportError:
39
+ _error(
40
+ "vgamepad not installed. Install with: pip install vgamepad "
41
+ "ViGEmBus driver also required: https://github.com/nefarius/ViGEmBus/releases",
42
+ "VIGEMBUS_MISSING"
43
+ )
44
+ sys.exit(1)
45
+ except Exception as e:
46
+ # vgamepad imports successfully but ViGEmBus service is not running/installed
47
+ _error(
48
+ f"vgamepad loaded but ViGEmBus driver error: {e} "
49
+ "Install ViGEmBus: https://github.com/nefarius/ViGEmBus/releases",
50
+ "VIGEMBUS_MISSING"
51
+ )
52
+ sys.exit(1)
53
+
54
+ try:
55
+ import pyautogui
56
+ pyautogui.FAILSAFE = False
57
+ pyautogui.PAUSE = 0 # Eliminate artificial delays during KBM injection
58
+ KBM_ENABLED = True
59
+ _log("pyautogui loaded — KBM passthrough enabled")
60
+ except ImportError:
61
+ _log("WARNING: pyautogui not installed — KBM passthrough disabled. Install: pip install pyautogui")
62
+ KBM_ENABLED = False
63
+
64
+
65
+ # ── Key map: KEY_* tokens → pyautogui key names ──────────────────────────────
66
+
67
+ PYAUTOGUI_KEY_MAP = {
68
+ "KEY_A": "a", "KEY_B": "b", "KEY_C": "c", "KEY_D": "d",
69
+ "KEY_E": "e", "KEY_F": "f", "KEY_G": "g", "KEY_H": "h",
70
+ "KEY_I": "i", "KEY_J": "j", "KEY_K": "k", "KEY_L": "l",
71
+ "KEY_M": "m", "KEY_N": "n", "KEY_O": "o", "KEY_P": "p",
72
+ "KEY_Q": "q", "KEY_R": "r", "KEY_S": "s", "KEY_T": "t",
73
+ "KEY_U": "u", "KEY_V": "v", "KEY_W": "w", "KEY_X": "x",
74
+ "KEY_Y": "y", "KEY_Z": "z",
75
+ "KEY_0": "0", "KEY_1": "1", "KEY_2": "2", "KEY_3": "3",
76
+ "KEY_4": "4", "KEY_5": "5", "KEY_6": "6", "KEY_7": "7",
77
+ "KEY_8": "8", "KEY_9": "9",
78
+ "KEY_UP": "up", "KEY_DOWN": "down", "KEY_LEFT": "left", "KEY_RIGHT": "right",
79
+ "KEY_SPACE": "space", "KEY_ENTER": "enter", "KEY_ESC": "escape",
80
+ "KEY_LEFTSHIFT": "shift", "KEY_RIGHTSHIFT": "shift",
81
+ "KEY_LEFTCTRL": "ctrl", "KEY_RIGHTCTRL": "ctrl",
82
+ "KEY_LEFTALT": "alt", "KEY_RIGHTALT": "alt",
83
+ "KEY_TAB": "tab", "KEY_BACKSPACE": "backspace",
84
+ "KEY_CAPSLOCK": "capslock",
85
+ "KEY_F1": "f1", "KEY_F2": "f2", "KEY_F3": "f3", "KEY_F4": "f4",
86
+ "KEY_F5": "f5", "KEY_F6": "f6", "KEY_F7": "f7", "KEY_F8": "f8",
87
+ "KEY_F9": "f9", "KEY_F10": "f10", "KEY_F11": "f11", "KEY_F12": "f12",
88
+ "BTN_LEFT": "left",
89
+ "BTN_MIDDLE": "middle",
90
+ "BTN_RIGHT": "right",
91
+ }
92
+
93
+ # ── Module state ──────────────────────────────────────────────────────────────
94
+
95
+ # devices: pad_id → VX360Gamepad instance
96
+ devices = {}
97
+ # viewer_modes: viewer_id → 'gamepad' | 'kbm' | 'hybrid' | 'kbm_emulated'
98
+ viewer_modes = {}
99
+ devices_by_slot = {}
100
+ event_queue = queue.Queue()
101
+
102
+
103
+ # ── Axis conversion helpers ───────────────────────────────────────────────────
104
+
105
+ def _clamp(value: float, lo: float, hi: float) -> float:
106
+ """Clamp a float to [lo, hi]."""
107
+ return max(lo, min(hi, value))
108
+
109
+
110
+ def _axis_to_float(raw) -> float:
111
+ """
112
+ Convert the raw axis value from Node.js payload to vgamepad float range [-1.0, +1.0].
113
+
114
+ Node sends axes as integers in the range -32767..+32767 (matching the
115
+ W3C Gamepad API specification). vgamepad expects -1.0..+1.0.
116
+
117
+ Division by 32767.0 (not 32768) avoids overflow at the negative extreme:
118
+ -32767 / 32767.0 = -1.0 exactly
119
+ +32767 / 32767.0 = +1.0 exactly
120
+ """
121
+ try:
122
+ return _clamp(float(raw) / 32767.0, -1.0, 1.0)
123
+ except (TypeError, ValueError):
124
+ return 0.0
125
+
126
+
127
+ def _trigger_to_float(raw) -> float:
128
+ """
129
+ Convert raw trigger value to vgamepad float range [0.0, +1.0].
130
+
131
+ Node sends trigger axes as integers in 0..255.
132
+ vgamepad expects 0.0..1.0.
133
+ """
134
+ try:
135
+ return _clamp(float(raw) / 255.0, 0.0, 1.0)
136
+ except (TypeError, ValueError):
137
+ return 0.0
138
+
139
+
140
+ # ── Button application helper ─────────────────────────────────────────────────
141
+
142
+ def _apply_btn(gp, btns: list, idx: int, const):
143
+ """Press or release a vgamepad button based on the W3C buttons array."""
144
+ if idx >= len(btns):
145
+ return
146
+ entry = btns[idx]
147
+ # W3C GamepadButton is { pressed: bool, value: float }
148
+ # Node may send it as a dict or as a bare bool/number depending on the path.
149
+ pressed = False
150
+ if isinstance(entry, dict):
151
+ pressed = bool(entry.get("pressed", False))
152
+ else:
153
+ pressed = bool(entry)
154
+
155
+ if pressed:
156
+ gp.press_button(button=const)
157
+ else:
158
+ gp.release_button(button=const)
159
+
160
+
161
+ # ── Main run loop ─────────────────────────────────────────────────────────────
162
+
163
+ def stdin_thread():
164
+ stdin_raw = open(sys.stdin.fileno(), 'rb', buffering=0)
165
+ for raw_line in stdin_raw:
166
+ line = raw_line.decode('utf-8', errors='replace').strip()
167
+ if line:
168
+ event_queue.put(('json', line))
169
+
170
+ def udp_thread(sock):
171
+ while True:
172
+ try:
173
+ data, _ = sock.recvfrom(1024)
174
+ if data and len(data) == 16 and data[0] == 0x01:
175
+ slot = data[15]
176
+ event_queue.put(('binary', slot, data))
177
+ except Exception:
178
+ pass
179
+
180
+ def _emit_gp_binary(slot, payload):
181
+ gp = devices_by_slot.get(slot)
182
+ if not gp: return
183
+
184
+ magic, lx, ly, rx, ry, lt, rt, cppBtns, hx, hy, slot_check = struct.unpack('<BhhhhBBHbbB', payload)
185
+
186
+ def _bit(mask): return bool(cppBtns & mask)
187
+ def _press(const, state):
188
+ if state: gp.press_button(button=const)
189
+ else: gp.release_button(button=const)
190
+
191
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_A, _bit(1 << 0))
192
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_B, _bit(1 << 1))
193
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_X, _bit(1 << 2))
194
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_Y, _bit(1 << 3))
195
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_SHOULDER, _bit(1 << 4))
196
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_SHOULDER, _bit(1 << 5))
197
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_BACK, _bit(1 << 8))
198
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_START, _bit(1 << 9))
199
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_THUMB, _bit(1 << 10))
200
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_THUMB, _bit(1 << 11))
201
+
202
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_UP, hy == -1)
203
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_DOWN, hy == 1)
204
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_LEFT, hx == -1)
205
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_RIGHT, hx == 1)
206
+
207
+ lx_f = _clamp(lx / 32767.0, -1.0, 1.0)
208
+ ly_f = -_clamp(ly / 32767.0, -1.0, 1.0)
209
+ gp.left_joystick_float(x_value_float=lx_f, y_value_float=ly_f)
210
+
211
+ rx_f = _clamp(rx / 32767.0, -1.0, 1.0)
212
+ ry_f = -_clamp(ry / 32767.0, -1.0, 1.0)
213
+ gp.right_joystick_float(x_value_float=rx_f, y_value_float=ry_f)
214
+
215
+ lt_f = _clamp(lt / 255.0, 0.0, 1.0)
216
+ rt_f = _clamp(rt / 255.0, 0.0, 1.0)
217
+ gp.left_trigger_float(value_float=lt_f)
218
+ gp.right_trigger_float(value_float=rt_f)
219
+
220
+ gp.update()
221
+
222
+ def run():
223
+ _emit({"type": "ready", "message": "Windows vgamepad + pyautogui backend initialized"})
224
+
225
+ udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
226
+ udp_sock.bind(('127.0.0.1', 0))
227
+ udp_port = udp_sock.getsockname()[1]
228
+ _emit({"type": "udp_ready", "udp_port": udp_port})
229
+
230
+ threading.Thread(target=stdin_thread, daemon=True).start()
231
+ threading.Thread(target=udp_thread, args=(udp_sock,), daemon=True).start()
232
+
233
+ while True:
234
+ ev = event_queue.get()
235
+ if ev[0] == 'binary':
236
+ _emit_gp_binary(ev[1], ev[2])
237
+ continue
238
+
239
+ line = ev[1]
240
+ try:
241
+ msg = json.loads(line)
242
+ except json.JSONDecodeError:
243
+ continue
244
+
245
+ try:
246
+ _process(msg)
247
+ except Exception as e:
248
+ _error(f"Unexpected error processing message: {e}", "PROCESS_ERROR")
249
+
250
+
251
+ def _process(msg: dict):
252
+ msg_type = msg.get("type", "")
253
+ vid = str(msg.get("viewer_id", msg.get("viewerId", "")))
254
+
255
+ # ── Mode updates ──────────────────────────────────────────────────────────
256
+ if msg_type == "set-input-mode":
257
+ viewer_modes[str(msg.get("viewerId", vid))] = msg.get("mode", "gamepad")
258
+ return
259
+
260
+ if msg_type == "allocate_slot":
261
+ pad_id = str(msg.get("pad_id", ""))
262
+ slot = msg.get("slot")
263
+ if pad_id not in devices:
264
+ # We don't have the gamepad instance yet, _handle_gamepad will lazy-create it.
265
+ # We can lazy-create it right here instead.
266
+ _handle_gamepad({"pad_id": pad_id})
267
+
268
+ if pad_id in devices:
269
+ devices_by_slot[slot] = devices[pad_id]
270
+ return
271
+
272
+ if msg_type == "free_slot":
273
+ slot = msg.get("slot")
274
+ if slot in devices_by_slot:
275
+ del devices_by_slot[slot]
276
+ return
277
+
278
+ # ── Viewer cleanup ────────────────────────────────────────────────────────
279
+ if msg_type in ("flush_neutral", "disconnect_viewer", "destroy_all"):
280
+ # Find all pad slots belonging to this viewer (pad IDs are "viewerId_N")
281
+ keys_to_remove = [
282
+ k for k in list(devices.keys())
283
+ if str(k).startswith(vid + "_") or str(k) == vid
284
+ ]
285
+ for k in keys_to_remove:
286
+ gp = devices.pop(k, None)
287
+ if gp is not None:
288
+ try:
289
+ # Zero out the controller state before destroying
290
+ gp.left_joystick(x_value=0, y_value=0)
291
+ gp.right_joystick(x_value=0, y_value=0)
292
+ gp.left_trigger(value=0)
293
+ gp.right_trigger(value=0)
294
+ gp.update()
295
+ except Exception:
296
+ pass
297
+ del gp
298
+ if msg_type == "destroy_all":
299
+ devices.clear()
300
+ gc.collect()
301
+ return
302
+
303
+ # ── Current mode for this viewer ─────────────────────────────────────────
304
+ current_mode = viewer_modes.get(vid, "gamepad")
305
+
306
+ # ── KBM handling ─────────────────────────────────────────────────────────
307
+ if msg_type in ("kbm", "keyboard"):
308
+ if not KBM_ENABLED:
309
+ return
310
+ if current_mode not in ("kbm", "hybrid", "kbm_emulated"):
311
+ return
312
+ _handle_kbm(msg)
313
+ return
314
+
315
+ # ── Gamepad handling ─────────────────────────────────────────────────────
316
+ if msg_type == "gamepad":
317
+ if current_mode not in ("gamepad", "hybrid"):
318
+ return
319
+ _handle_gamepad(msg)
320
+ return
321
+
322
+
323
+ def _handle_kbm(msg: dict):
324
+ """Inject keyboard/mouse events via pyautogui."""
325
+ event_type = msg.get("event", "")
326
+
327
+ if event_type == "mousemove":
328
+ dx = msg.get("dx", 0)
329
+ dy = msg.get("dy", 0)
330
+ if dx != 0 or dy != 0:
331
+ try:
332
+ pyautogui.move(int(dx), int(dy))
333
+ except Exception:
334
+ pass
335
+ return
336
+
337
+ if event_type in ("keydown", "keyup"):
338
+ key_name = msg.get("key", "")
339
+ py_key = PYAUTOGUI_KEY_MAP.get(key_name)
340
+ if py_key is None:
341
+ # Fall back: strip KEY_ prefix and lower-case
342
+ py_key = key_name.lower().replace("key_", "")
343
+
344
+ try:
345
+ is_mouse = "btn_" in key_name.lower()
346
+ if is_mouse:
347
+ if event_type == "keydown":
348
+ pyautogui.mouseDown(button=py_key)
349
+ else:
350
+ pyautogui.mouseUp(button=py_key)
351
+ else:
352
+ if event_type == "keydown":
353
+ pyautogui.keyDown(py_key)
354
+ else:
355
+ pyautogui.keyUp(py_key)
356
+ except Exception:
357
+ pass
358
+
359
+
360
+ def _handle_gamepad(msg: dict):
361
+ """Inject gamepad state via vgamepad VX360Gamepad."""
362
+ # pad_id is set by InputOrchestrator: 'viewerId_0', 'viewerId_1', etc.
363
+ pad_id = str(msg.get("pad_id", msg.get("viewer_id", "default")))
364
+
365
+ # Lazy-create the virtual gamepad for this slot
366
+ if pad_id not in devices:
367
+ try:
368
+ gp = vg.VX360Gamepad()
369
+
370
+ # Capture pad_id in a closure — vgamepad 0.1.0's register_notification()
371
+ # does not accept a user_data kwarg, but still passes user_data=None to
372
+ # the callback, so we ignore the callback's user_data and use our own.
373
+ _captured_pad_id = pad_id
374
+ def _on_vibration(client, target, large_motor, small_motor, led_number, user_data):
375
+ vid = str(_captured_pad_id).split('_')[0] if "_" in str(_captured_pad_id) else str(_captured_pad_id)
376
+ _emit({
377
+ "type": "rumble",
378
+ "viewerId": vid,
379
+ "strong": large_motor / 255.0,
380
+ "weak": small_motor / 255.0,
381
+ "duration": 250
382
+ })
383
+
384
+ gp.register_notification(callback_function=_on_vibration)
385
+
386
+ gp.update() # Send an initial neutral state to register with ViGEmBus
387
+ devices[pad_id] = gp
388
+ _log(f"Created virtual gamepad for slot: {pad_id}")
389
+ except Exception as e:
390
+ _error(
391
+ f"Failed to create VX360Gamepad for {pad_id}: {e} "
392
+ "Ensure ViGEmBus is installed: https://github.com/nefarius/ViGEmBus/releases",
393
+ "VIGEMBUS_CREATE_FAILED"
394
+ )
395
+ return
396
+
397
+ gp = devices[pad_id]
398
+
399
+ # Node sends:
400
+ # buttons: integer bitmask (from InputOrchestrator's _gpBuf)
401
+ # axes: [lx, ly, rx, ry, lt, rt] where lx/ly/rx/ry are -32767..+32767
402
+ # and lt/rt are 0..255
403
+ # OR (legacy W3C array path):
404
+ # buttons: array of { pressed, value }
405
+ # axes: array of raw floats
406
+
407
+ btns_raw = msg.get("buttons", 0)
408
+ axes_raw = msg.get("axes", [])
409
+
410
+ try:
411
+ if isinstance(btns_raw, int) or "lx" in msg:
412
+ # Bitmask or flat schema path — from InputOrchestrator's binary protocol
413
+ _apply_bitmask(gp, btns_raw, axes_raw, msg)
414
+ else:
415
+ # Legacy W3C array path — direct from viewer.js (pre-orchestrator)
416
+ _apply_w3c_array(gp, btns_raw, axes_raw)
417
+
418
+ gp.update()
419
+
420
+ except Exception as e:
421
+ _error(f"Error updating gamepad {pad_id}: {e}", "GAMEPAD_UPDATE_ERROR")
422
+
423
+
424
+ def _apply_bitmask(gp, buttons: int, axes: list, msg: dict = None):
425
+ """
426
+ Apply gamepad state from InputOrchestrator's compact bitmask + axes format.
427
+
428
+ Bitmask layout (matches InputOrchestrator KBM_BTN_MAP):
429
+ bit 0 (0x0001): A
430
+ bit 1 (0x0002): B
431
+ bit 2 (0x0004): X
432
+ bit 3 (0x0008): Y
433
+ bit 4 (0x0010): D-Up
434
+ bit 5 (0x0020): D-Down
435
+ bit 6 (0x0040): D-Left
436
+ bit 7 (0x0080): D-Right
437
+ bit 8 (0x0100): LB
438
+ bit 9 (0x0200): RB
439
+ bit 10 (0x0400): L3
440
+ bit 11 (0x0800): R3
441
+ bit 12 (0x1000): Start
442
+ bit 13 (0x2000): Select/Back
443
+ bit 14 (0x4000): Guide
444
+ """
445
+ if isinstance(buttons, int):
446
+ def _bit(mask): return bool(buttons & mask)
447
+ def _press(const, state):
448
+ if state: gp.press_button(button=const)
449
+ else: gp.release_button(button=const)
450
+
451
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_A, _bit(0x0001))
452
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_B, _bit(0x0002))
453
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_X, _bit(0x0004))
454
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_Y, _bit(0x0008))
455
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_UP, _bit(0x0010))
456
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_DOWN, _bit(0x0020))
457
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_LEFT, _bit(0x0040))
458
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_RIGHT, _bit(0x0080))
459
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_SHOULDER, _bit(0x0100))
460
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_SHOULDER, _bit(0x0200))
461
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_THUMB, _bit(0x0400))
462
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_THUMB, _bit(0x0800))
463
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_START, _bit(0x1000))
464
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_BACK, _bit(0x2000))
465
+ _press(vg.XUSB_BUTTON.XUSB_GAMEPAD_GUIDE, _bit(0x4000))
466
+
467
+ if msg is not None and "lx" in msg:
468
+ lx = _axis_to_float(msg.get("lx", 0))
469
+ ly = -_axis_to_float(msg.get("ly", 0))
470
+ gp.left_joystick_float(x_value_float=lx, y_value_float=ly)
471
+
472
+ rx = _axis_to_float(msg.get("rx", 0))
473
+ ry = -_axis_to_float(msg.get("ry", 0))
474
+ gp.right_joystick_float(x_value_float=rx, y_value_float=ry)
475
+
476
+ lt = float(msg.get("lt", 0.0))
477
+ rt = float(msg.get("rt", 0.0))
478
+ gp.left_trigger_float(value_float=lt)
479
+ gp.right_trigger_float(value_float=rt)
480
+ else:
481
+ # Fallback to reading from legacy axes array if present
482
+ if len(axes) >= 2:
483
+ lx = _axis_to_float(axes[0])
484
+ ly = -_axis_to_float(axes[1])
485
+ gp.left_joystick_float(x_value_float=lx, y_value_float=ly)
486
+
487
+ if len(axes) >= 4:
488
+ rx = _axis_to_float(axes[2])
489
+ ry = -_axis_to_float(axes[3])
490
+ gp.right_joystick_float(x_value_float=rx, y_value_float=ry)
491
+
492
+ if len(axes) >= 6:
493
+ lt = _trigger_to_float(axes[4])
494
+ rt = _trigger_to_float(axes[5])
495
+ gp.left_trigger_float(value_float=lt)
496
+ gp.right_trigger_float(value_float=rt)
497
+
498
+
499
+ def _apply_w3c_array(gp, btns: list, axes: list):
500
+ """
501
+ Apply gamepad state from the legacy W3C Gamepad API array format.
502
+ Used when the packet arrives directly from viewer.js (not via orchestrator bitmask).
503
+
504
+ btns: list of { pressed: bool, value: float } or bare booleans
505
+ axes: list of raw floats -1.0..+1.0 from Gamepad.axes
506
+ """
507
+ _apply_btn(gp, btns, 0, vg.XUSB_BUTTON.XUSB_GAMEPAD_A)
508
+ _apply_btn(gp, btns, 1, vg.XUSB_BUTTON.XUSB_GAMEPAD_B)
509
+ _apply_btn(gp, btns, 2, vg.XUSB_BUTTON.XUSB_GAMEPAD_X)
510
+ _apply_btn(gp, btns, 3, vg.XUSB_BUTTON.XUSB_GAMEPAD_Y)
511
+ _apply_btn(gp, btns, 4, vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_SHOULDER)
512
+ _apply_btn(gp, btns, 5, vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_SHOULDER)
513
+ _apply_btn(gp, btns, 8, vg.XUSB_BUTTON.XUSB_GAMEPAD_BACK)
514
+ _apply_btn(gp, btns, 9, vg.XUSB_BUTTON.XUSB_GAMEPAD_START)
515
+ _apply_btn(gp, btns, 10, vg.XUSB_BUTTON.XUSB_GAMEPAD_LEFT_THUMB)
516
+ _apply_btn(gp, btns, 11, vg.XUSB_BUTTON.XUSB_GAMEPAD_RIGHT_THUMB)
517
+ _apply_btn(gp, btns, 12, vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_UP)
518
+ _apply_btn(gp, btns, 13, vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_DOWN)
519
+ _apply_btn(gp, btns, 14, vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_LEFT)
520
+ _apply_btn(gp, btns, 15, vg.XUSB_BUTTON.XUSB_GAMEPAD_DPAD_RIGHT)
521
+ _apply_btn(gp, btns, 16, vg.XUSB_BUTTON.XUSB_GAMEPAD_GUIDE)
522
+
523
+ # W3C axes are already in -1.0..+1.0 range; clamp defensively.
524
+ # Negate Y axes: W3C Y+ = down, ViGEm Y+ = up.
525
+ if len(axes) >= 2:
526
+ lx = _clamp(float(axes[0]), -1.0, 1.0)
527
+ ly = -_clamp(float(axes[1]), -1.0, 1.0)
528
+ gp.left_joystick_float(x_value_float=lx, y_value_float=ly)
529
+
530
+ if len(axes) >= 4:
531
+ rx = _clamp(float(axes[2]), -1.0, 1.0)
532
+ ry = -_clamp(float(axes[3]), -1.0, 1.0)
533
+ gp.right_joystick_float(x_value_float=rx, y_value_float=ry)
534
+
535
+ # W3C triggers are axes[4]/axes[5] in 0.0..1.0 (some browsers put them in -1..+1)
536
+ if len(axes) >= 6:
537
+ lt = _clamp((float(axes[4]) + 1.0) / 2.0, 0.0, 1.0)
538
+ rt = _clamp((float(axes[5]) + 1.0) / 2.0, 0.0, 1.0)
539
+ gp.left_trigger_float(value_float=lt)
540
+ gp.right_trigger_float(value_float=rt)
541
+
542
+
543
+ if __name__ == "__main__":
544
+ run()
@@ -0,0 +1,14 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <OutputType>Exe</OutputType>
4
+ <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
5
+ <RuntimeIdentifier>win-x64</RuntimeIdentifier>
6
+ <PlatformTarget>x64</PlatformTarget>
7
+ <Nullable>enable</Nullable>
8
+ <RootNamespace>HmBridge</RootNamespace>
9
+ <AssemblyName>HmBridge</AssemblyName>
10
+ </PropertyGroup>
11
+ <ItemGroup>
12
+ <ProjectReference Include="..\..\hidmaestro\sdk\HIDMaestro.Core\HIDMaestro.Core.csproj" />
13
+ </ItemGroup>
14
+ </Project>
Binary file