@liminal-hq/plugin-gamepad-haptics 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # tauri-plugin-gamepad-haptics
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Rumble for gamepads from one pattern language. It plays on the pad and never reads its input, so it sits beside the webview's Gamepad API without competing for the device.
4
+
5
+ ## Package Links
6
+
7
+ - Rust crate: `tauri-plugin-gamepad-haptics`
8
+ - npm package: `@liminal-hq/plugin-gamepad-haptics`
9
+
10
+ ## Installation
11
+
12
+ ```toml
13
+ [dependencies]
14
+ tauri-plugin-gamepad-haptics = { path = "../../plugins/gamepad-haptics" }
15
+ ```
16
+
17
+ ```rust
18
+ tauri::Builder::default().plugin(tauri_plugin_gamepad_haptics::init())
19
+ ```
20
+
21
+ Add `"gamepad-haptics:default"` to your capability, plus `core:event:default` to listen for pad events.
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { createBackend, PATTERN_FORMAT } from '@liminal-hq/plugin-gamepad-haptics';
27
+
28
+ const pads = createBackend();
29
+ await pads.register('hurt', {
30
+ format: PATTERN_FORMAT,
31
+ events: [
32
+ { type: 'transient', at: 0, intensity: 1, sharpness: 0.9 },
33
+ {
34
+ type: 'continuous',
35
+ at: 40,
36
+ duration: 120,
37
+ intensity: [
38
+ { t: 0, v: 0.9 },
39
+ { t: 1, v: 0 },
40
+ ],
41
+ sharpness: 0.1,
42
+ },
43
+ ],
44
+ });
45
+ await pads.trigger('hurt'); // plays on the first pad that can rumble
46
+ ```
47
+
48
+ A pattern is transients (a tap with intensity and sharpness) and continuous events (a hum with curves), the same format the phone plugin uses. Sharpness decides which motor plays: crisp goes to the light, high-frequency motor and dull goes to the heavy, low-frequency one.
49
+
50
+ ### The ladder
51
+
52
+ | Tier | Plays |
53
+ | ---- | --------------------------------------------------------------------------------------------- |
54
+ | 3 | Triggers and both body motors (not yet built) |
55
+ | 2 | Both body motors, mixed by sharpness (a pattern that only ever uses one motor reports tier 1) |
56
+ | 1 | One motor, the stronger of the two |
57
+ | 0 | Nothing; resolves `{ ok: true, tier: 0, reason }` |
58
+
59
+ A pad that cannot play never throws. Results report the tier that played and why it was lower.
60
+
61
+ ## API
62
+
63
+ - `createBackend()` returns `register`, `trigger`, `setMasterScale`, `setMaxTier`, `stop` and `capabilities`.
64
+ - `listPads()`, `capabilities()`, `playFrames(padId, frames, scale?)`, `identify(padId)` and `stop(padId?)` call the plugin directly.
65
+ - `onPadConnected`, `onPadChanged` and `onPadDisconnected` follow hot-plug. Pads get a slot (`gamepad:0`, `gamepad:1`, ...) and a returning pad gets its old slot back while the app runs.
66
+ - `resolvePad(pads, hint)` finds the plugin's pad for a Web `Gamepad`, an SDL GUID, or a vendor, product and serial. The GUID is the same one SDL and gilrs report. Two identical pads resolve as `ambiguous`; call `identify(padId)` and ask the player which one buzzed.
67
+
68
+ ## Where requests are validated
69
+
70
+ Rust validates every request before a backend sees it: at most 512 frames, levels within 0..1, durations of at least 1 ms, and a total no longer than `maxDurationMs`. A scale of 0, a pad that cannot play and `setMaxTier` never let an invalid request through. A motor is never left running longer than `maxContinuousMs`; the excess is cut and the timing after it kept. A shared corpus (`tests/conformance/frames.json`) is checked by both the Rust and the guest suites, and both sides are fuzzed.
71
+
72
+ ## Configuration
73
+
74
+ ```json
75
+ {
76
+ "plugins": {
77
+ "gamepad-haptics": {
78
+ "maxDurationMs": 3000,
79
+ "maxContinuousMs": 2000,
80
+ "masterScale": 1,
81
+ "stopOnBlur": true
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ Every pad stops when the app exits, when its pad is unplugged, and (with `stopOnBlur`) when the window loses focus.
88
+
89
+ ## Living beside the Gamepad API
90
+
91
+ The plugin never opens a pad for input and never grabs it, so the webview reads buttons and sticks as usual. Rumble is the one thing two writers could fight over, so each pad has one writer:
92
+
93
+ - A pad the plugin can address is played natively, always.
94
+ - Only when no native pad fits does the guest play through `navigator.getGamepads()[n].vibrationActuator` (`dual-rumble`). That path needs a webview that supports it: WebView2 and WKWebView on macOS do; WebKitGTK does not yet in current releases, and Android's WebView does not.
95
+ - Do not call `playEffect` yourself on a pad this plugin plays. On Linux the kernel adds rumble from different writers together; elsewhere the last writer wins.
96
+ - The webview lists a pad only after a button is pressed on the page, so a web `Gamepad` hint may not exist yet at start-up. Native pads are listed at once.
97
+
98
+ ## Platform support
99
+
100
+ | Platform | Backend | Status |
101
+ | -------- | -------------------- | --------------------------------------------- |
102
+ | Linux | evdev force feedback | Works; checked on DualShock 3 |
103
+ | Windows | Web fallback only | Native backend planned (Windows.Gaming.Input) |
104
+ | macOS | Web fallback only | Native backend planned (GameController) |
105
+ | Android | None | Planned (`InputDevice` vibrator, API 31+) |
106
+ | iOS | None | Planned (GameController) |
107
+
108
+ ### Linux access
109
+
110
+ The plugin opens `/dev/input/event*` for writing. On most desktops the logged-in user already has access to gamepads through a udev `uaccess` rule. A pad that cannot be opened is listed at tier 0 with `No write access to /dev/input/eventN`. In Flatpak, grant `--device=input` (Flatpak 1.15.6 or later); in Snap, connect the `joystick` interface.
111
+
112
+ DualShock 3 pads use the `hid-sony` driver, whose light motor only switches on and off, and which needs about 100 ms to spin up. For a pad like that (`lightBinary: true` in `PadInfo`) the plugin shapes the light motor: a short strong tap becomes one full 100 ms pulse, an in-between level becomes pulses of at least 100 ms in proportion to the level, and a level too faint to fill a pulse is dropped. A pulse that runs past the end of a pattern extends it, within `maxDurationMs`. The heavy motor and all other timing are unchanged, and a result that was shaped says so in its `reason`.
113
+
114
+ DualShock 4 pads (`weakHeavy: true`) have a heavy motor that does not spin up for a short soft tap: 60 ms at 0.6 is not felt, while 90 ms at 0.6 and 60 ms at 1.0 are. For those pads a heavy pulse shorter than 90 ms that never reaches 0.7 is lengthened to 90 ms into the silent heavy time after it, or past the end within `maxDurationMs`, and the result says so in its `reason`.
115
+
116
+ A pad that appears is sent a stop first, because some pads keep the last rumble they were given after a cable is pulled.
117
+
118
+ Hints saved from a pad's name or GUID do not survive a switch between USB and Bluetooth (the name and GUID differ); the serial (the pad's Bluetooth address) is the same on both, so prefer `{ vendorId, productId, serial }`. WebKitGTK reports a pad's Web `id` without vendor and product ids, so `resolvePad` matches it by name there.
119
+
120
+ ### Checking a pad
121
+
122
+ ```sh
123
+ cargo run -p tauri-plugin-gamepad-haptics --example probe # list pads
124
+ cargo run -p tauri-plugin-gamepad-haptics --example probe -- identify gamepad:0
125
+ cargo run -p tauri-plugin-gamepad-haptics --example probe -- play gamepad:0
126
+ cargo run -p tauri-plugin-gamepad-haptics --example probe -- watch # hot-plug events
127
+ ```
128
+
129
+ ## Permissions
130
+
131
+ `gamepad-haptics:default` allows `capabilities`, `identify`, `list_pads`, `play_frames` and `stop`.
132
+
133
+ ## Not yet tested on hardware
134
+
135
+ DualSense, Xbox and Switch Pro pads, triggers, Windows, macOS, Android and iOS.