@gjsify/gamepad 0.52.0 → 0.54.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 +63 -5
- package/lib/esm/axis-mapping.js +1 -1
- package/lib/esm/backend.js +1 -1
- package/lib/esm/compare-source.js +2 -0
- package/lib/esm/gamepad-manager.js +1 -1
- package/lib/esm/haptic-actuator.js +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/manette-source.js +1 -0
- package/lib/esm/register.js +1 -1
- package/lib/esm/sdl-namespace.js +0 -0
- package/lib/esm/sdl-source.js +1 -0
- package/lib/esm/source.js +1 -0
- package/lib/types/axis-mapping.d.ts +14 -7
- package/lib/types/backend.d.ts +94 -24
- package/lib/types/compare-source.d.ts +31 -0
- package/lib/types/compare-source.spec.d.ts +2 -0
- package/lib/types/gamepad-manager.d.ts +23 -13
- package/lib/types/gamepad.d.ts +4 -0
- package/lib/types/haptic-actuator.d.ts +60 -9
- package/lib/types/hardware-check.d.mts +1 -0
- package/lib/types/index.d.ts +4 -2
- package/lib/types/manette-source.d.ts +19 -0
- package/lib/types/register.d.ts +1 -1
- package/lib/types/sdl-namespace.d.ts +28 -0
- package/lib/types/sdl-node-gi.spec.d.ts +2 -0
- package/lib/types/sdl-source.d.ts +29 -0
- package/lib/types/sdl-source.spec.d.ts +2 -0
- package/lib/types/source.d.ts +71 -0
- package/lib/types/source.spec.d.ts +2 -0
- package/lib/types/test.browser.d.mts +1 -0
- package/lib/types/test.node-gi.d.mts +1 -0
- package/package.json +26 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @gjsify/gamepad
|
|
2
2
|
|
|
3
|
-
The W3C Gamepad API for GJS
|
|
3
|
+
The W3C Gamepad API for GJS. Provides `navigator.getGamepads()` polling, `Gamepad`, `GamepadButton`, `gamepadconnected`/`gamepaddisconnected` events, and rumble via `GamepadHapticActuator`. The backend is SDL3 on macOS and Windows, through [`@gjsify/gamepad-native`](../gamepad-native/README.md), and libmanette 0.2 on Linux until SDL has been compared against it there; it starts lazily on the first `getGamepads()` call.
|
|
4
4
|
|
|
5
5
|
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
6
|
|
|
@@ -39,11 +39,49 @@ for (const pad of gamepads) {
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
`vibrationActuator` follows the spec on both backends: parameters default to 0, an
|
|
43
|
+
out-of-range one rejects with `TypeError`, an effect the pad lacks with
|
|
44
|
+
`NotSupportedError`. `playEffect()` resolves `'complete'` once the effect has played
|
|
45
|
+
(`startDelay + duration`, at most 5 s), or `'preempted'` when a later `playEffect()` or
|
|
46
|
+
`reset()` interrupts it.
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
## Platform support
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
| host | backend | status |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| Linux | libmanette 0.2 (`gi://Manette`) by default; the SDL3 shim with `GJSIFY_GAMEPAD_BACKEND=sdl`, or both with `=compare` | supported (libmanette); the shim reads a virtual uinput pad in CI |
|
|
53
|
+
| macOS | SDL3 behind a GObject shim (`gi://GjsifyGamepad`, `@gjsify/gamepad-native`) | partial: builds, loads and enumerates; input from a real controller not yet verified |
|
|
54
|
+
| Windows | the same shim, from Node through `@gjsify/node-gi` | partial: builds, loads, and reads a virtual XInput pad in CI; a real controller not yet verified |
|
|
55
|
+
|
|
56
|
+
The decision is [ADR 0075](../../../docs/adr/0075-darwin-gamepad-backend-is-sdl3-behind-a-gobject-shim.md) and its Amendment 1: SDL3, statically linked and trimmed to the input subsystems, becomes the one backend on every OS, and replaces libmanette on Linux once it has been compared against it there with real controllers. libmanette itself cannot move: it links `libevdev` unconditionally, and libevdev is packaged for Linux and FreeBSD only.
|
|
57
|
+
|
|
58
|
+
On macOS and Windows the typelib and library come from the per-target optional dependency `@gjsify/gamepad-native-<os>-<arch>`, and a process finds them through `gjsify run`, which puts every installed prebuild on `GI_TYPELIB_PATH` (and, on Windows, on `PATH`). Neither probes for libmanette.
|
|
59
|
+
|
|
60
|
+
### Comparing the two Linux backends
|
|
61
|
+
|
|
62
|
+
Amendment 1 removes libmanette only after the SDL3 shim has been compared against it on real controllers. `GJSIFY_GAMEPAD_BACKEND` selects the backend per process on Linux (macOS and Windows ignore it):
|
|
63
|
+
|
|
64
|
+
| value | the page sees | what else happens |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| unset / `manette` | libmanette | — |
|
|
67
|
+
| `sdl` | the SDL3 shim | — |
|
|
68
|
+
| `compare` | libmanette | the shim runs beside it on the same controllers; connects, disconnects and every control on which the two disagree for more than one poll go to stderr, prefixed `[@gjsify/gamepad compare]` |
|
|
69
|
+
|
|
70
|
+
Controllers are paired by USB vendor:product (read from each backend's GUID), then by connection order. A difference of up to 0.02 is scaling, not disagreement: libmanette normalises the evdev range, SDL rescales to int16 first. One difference is expected and is not a control: the two name the same pad differently (`Gamepad.id`), because SDL takes the name from its mapping database and libmanette from the kernel.
|
|
71
|
+
|
|
72
|
+
Found so far (ADR 0075, Amendment 1's measurements): on an 8BitDo N30 Pro 2 SDL is the correct one of the two. libmanette 0.2 reports no Home button and no right-stick Y for it, and fires the left trigger when the right stick is pushed down: its evdev backend matches a mapping's `aN` against the kernel axis CODE rather than the N-th axis the device has, which only coincides on pads whose axis codes have no gaps.
|
|
73
|
+
|
|
74
|
+
### Node
|
|
75
|
+
|
|
76
|
+
`gjsify.runtimes.node` is `partial`, and the same source runs under Node: a `--app node` bundle reaches `gi://` through [`@gjsify/node-gi`](../../node-gi/node-gi), so with the macOS prebuild installed the shim works there too. `@gjsify/node-gi` is NOT a dependency — a plain-Node program without it is a supported configuration and answers `hasGamepadBackend() === false` with a `console.warn` naming it.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install @gjsify/node-gi @gjsify/gamepad # plus @gjsify/gamepad-native-darwin-<arch>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Measured on macOS 27 arm64 / Node 24.21.0: the probe classifies `sdl`, `hasGamepadBackend()` is `true`, and `SdlSource` initialises, enumerates zero controllers and tears down over repeated cycles (`gjsify workspace @gjsify/gamepad run test:gjs-on-node`, 4 tests / 28 assertions). Input from a real controller is unverified on this target, exactly as on GJS.
|
|
83
|
+
|
|
84
|
+
A host with no backend — Linux without libmanette, macOS or Windows without the prebuild — is a platform gap, not a bug in this package, and it is reported, not hidden. There, `navigator.getGamepads()` returns the **empty list** *because there is no backend* — indistinguishable, from the return value alone, from a Linux host with nothing plugged in. That is deliberate, and it is what the spec asks for: `Navigator.[[gamepads]]` "is initially the empty list" and grows only when an index is selected for a connected device, so `getGamepads()`'s steps only ever return a list (their one `throw` is a `SecurityError` for the `"gamepad"` permission policy), and a browser on a machine with no gamepad driver returns exactly the same empty answer — WebKit compiles an `EmptyGamepadProvider` for precisely that case. Making the call throw would break every page that polls `navigator.getGamepads().length`.
|
|
47
85
|
|
|
48
86
|
Ask the capability export instead of guessing from an empty list:
|
|
49
87
|
|
|
@@ -58,10 +96,30 @@ if (!(await hasGamepadBackend())) {
|
|
|
58
96
|
|
|
59
97
|
`hasGamepadBackend()` needs no monitor and no connected device, and it is **quiet**: asking the question prints nothing, so the recommended usage above costs no stderr line on a macOS or Windows start. The one-time explanation comes from the *use* instead — the first `getGamepads()` that actually wanted a monitor:
|
|
60
98
|
|
|
61
|
-
* **no backend** (no Manette typelib; or `@gjsify/node-gi` not installed on the node target) → one `console.warn` naming what to install.
|
|
99
|
+
* **no backend** (no Manette typelib; no `@gjsify/gamepad-native` prebuild on macOS or Windows; or `@gjsify/node-gi` not installed on the node target) → one `console.warn` naming what to install.
|
|
62
100
|
* **a fault** (a shared library that will not `dlopen`, a version or ABI skew) → one `console.error` carrying the original error. A broken setup is a fault, not a platform gap, and the two must not look alike.
|
|
63
101
|
* **the monitor fails to start** after the backend loaded (no udev or `/dev/input` in a sandbox; flatpak: `--device=input`) → its own `console.error`, because that is a third, distinct failure.
|
|
64
102
|
|
|
103
|
+
## Driving the manager from your own device source
|
|
104
|
+
|
|
105
|
+
`GamepadManager` reads devices through a `GamepadSource`: an object with `start(sink)`, `stop()` and an optional `poll()`, which reports `connected` / `disconnected` / `button` / `axis` in the W3C standard layout. Hand one to the constructor to test gamepad handling with no controller attached, or to feed input you already own:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { GamepadManager, type GamepadSource, type GamepadSourceSink } from '@gjsify/gamepad';
|
|
109
|
+
|
|
110
|
+
const pad = { id: 'Scripted Pad', vibrationActuator: null };
|
|
111
|
+
let sink: GamepadSourceSink | null = null;
|
|
112
|
+
const source: GamepadSource = {
|
|
113
|
+
name: 'scripted',
|
|
114
|
+
start: (s) => { sink = s; s.connected(pad); },
|
|
115
|
+
stop: () => { sink = null; },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const manager = new GamepadManager({ source });
|
|
119
|
+
manager.getGamepads(); // starts the source: [Gamepad { id: 'Scripted Pad', index: 0, … }]
|
|
120
|
+
sink!.button(pad, 0, 1, true); // standard button 0 (A / Cross) pressed
|
|
121
|
+
```
|
|
122
|
+
|
|
65
123
|
On the browser and NativeScript targets the runtime's own Gamepad API is the implementation (`gjsify.runtimes` declares both `native`): the package root routes to `globals.mjs`, whose `hasGamepadBackend()` reports that native surface, and `@gjsify/gamepad/register` leaves an existing `navigator.getGamepads` untouched rather than replacing it.
|
|
66
124
|
|
|
67
125
|
## License
|
package/lib/esm/axis-mapping.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e={LEFT_X:0,LEFT_Y:1,RIGHT_X:
|
|
1
|
+
const e={LEFT_X:0,LEFT_Y:1,RIGHT_X:3,RIGHT_Y:4,LEFT_TRIGGER:2,RIGHT_TRIGGER:5},t={LEFT_STICK_X:0,LEFT_STICK_Y:1,RIGHT_STICK_X:2,RIGHT_STICK_Y:3},n=4,r=new Map([[e.LEFT_X,t.LEFT_STICK_X],[e.LEFT_Y,t.LEFT_STICK_Y],[e.RIGHT_X,t.RIGHT_STICK_X],[e.RIGHT_Y,t.RIGHT_STICK_Y]]),i=.5;export{r as MANETTE_TO_W3C_AXIS,e as ManetteAxis,i as TRIGGER_PRESS_THRESHOLD,t as W3CAxis,n as W3C_AXIS_COUNT};
|
package/lib/esm/backend.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";const
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{hostEnv as e,hostOs as t}from"@gjsify/utils/core";const n=`GJSIFY_GAMEPAD_BACKEND`,r=[`manette`,`sdl`,`compare`];function typelibAbsentNeedle(e){return`Typelib file for namespace '${e}'`}const i=`@gjsify/node-gi`;function loadFaultText(e,t){return`[@gjsify/gamepad] The gi://${e} gamepad backend failed to load — a fault on this host, NOT ${t}. getGamepads() reports no controllers until it is fixed.`}const a={status:`manette`,namespace:`Manette`,noTypelibText:`[@gjsify/gamepad] No gamepad backend on this host — the Manette-0.2 typelib is absent, so getGamepads() reports no controllers no matter what is plugged in. Install libmanette + its typelib (Fedora: libmanette; Debian/Ubuntu: gir1.2-manette-0.2); Windows has no libmanette at all. Gate on hasGamepadBackend().`,noBridgeText:`[@gjsify/gamepad] No gamepad backend in this process — ${i} is not installed, and it is how a --app node bundle reaches gi://Manette. getGamepads() reports no controllers until it is added (npm install ${i}, plus libmanette + its typelib). Gate on hasGamepadBackend().`,loadFaultText:loadFaultText(`Manette`,`a platform without libmanette`)},o={status:`sdl`,namespace:`GjsifyGamepad`,noTypelibText:"[@gjsify/gamepad] No gamepad backend on this host — the GjsifyGamepad-1.0 typelib of @gjsify/gamepad-native is not on the typelib path, so getGamepads() reports no controllers no matter what is plugged in. It ships as the optional dependency @gjsify/gamepad-native-<os>-<arch>: check that it installed, and start the program through `gjsify run` (or put its prebuilds/<os>-<arch> directory on GI_TYPELIB_PATH and on the library search path: PATH on Windows). Gate on hasGamepadBackend().",noBridgeText:`[@gjsify/gamepad] No gamepad backend in this process — ${i} is not installed, and it is how a --app node bundle reaches gi://GjsifyGamepad. getGamepads() reports no controllers until it is added (npm install ${i}). Gate on hasGamepadBackend().`,loadFaultText:loadFaultText(`GjsifyGamepad`,`a host without @gjsify/gamepad-native`)},s={manette:a,sdl:o};let c=null,l=!1;function isEmptiedGiModule(e){return typeof e==`object`&&!!e&&Object.getPrototypeOf(e)===Object.prototype&&Object.getOwnPropertyNames(e).length===0}function isMissingNodeGiBridge(e,t){if(!t.includes(i))return!1;let n=e?.code;return n===`MODULE_NOT_FOUND`||n===`ERR_MODULE_NOT_FOUND`||t.includes(`Cannot find module`)}function _diagnoseGiLoadError(e,t=a){let n=e instanceof Error?e.message:String(e);return n.includes(typelibAbsentNeedle(t.namespace))?{status:`absent`,diagnostic:t.noTypelibText}:isMissingNodeGiBridge(e,n)?{status:`absent`,diagnostic:t.noBridgeText}:{status:`failed`,diagnostic:t.loadFaultText}}async function probeGi(e,t){let n;try{if(n=(await t()).default,typeof n?.Monitor!=`function`)return isEmptiedGiModule(n)?{status:`absent`,module:null,error:null,diagnostic:null,shadow:null}:{status:`failed`,module:null,error:Error(`gi://${e.namespace} resolved without a Monitor class — unexpected ${e.namespace} ABI`),diagnostic:e.loadFaultText,shadow:null}}catch(t){let{status:n,diagnostic:r}=_diagnoseGiLoadError(t,e);return{status:n,module:null,error:t,diagnostic:r,shadow:null}}return e.status===`sdl`?{status:`sdl`,module:n,error:null,diagnostic:null,shadow:null}:{status:`manette`,module:n,error:null,diagnostic:null,shadow:null}}function readBackendChoice(e){let t=e(n);if(t===void 0||t===``)return{choice:`manette`,note:null};let i=t.trim().toLowerCase();return r.includes(i)?{choice:i,note:null}:{choice:`manette`,note:`[@gjsify/gamepad] ${n}=${JSON.stringify(t)} is not one of ${r.join(`, `)}; using manette.`}}async function probeGamepadBackend(n){let probeSdl=()=>probeGi(o,n.sdlImporter??(()=>import(`gi://GjsifyGamepad?version=1.0`))),probeManette=()=>probeGi(a,n.importer??(()=>import(`gi://Manette?version=0.2`))),r=(n.hostOs??t)();if(r===`darwin`||r===`win32`)return probeSdl();let{choice:i,note:s}=readBackendChoice(n.env??e);if(i===`sdl`)return probeSdl();let c=await probeManette();return s===null?i===`compare`?{...c,shadow:await probeSdl()}:c:{...c,diagnostic:c.diagnostic??s}}function loadGamepadBackend(e={}){if(c!==null){if(e.importer||e.sdlImporter||e.hostOs||e.env)throw Error(`loadGamepadBackend({ importer, sdlImporter, hostOs, env }) called after the probe already ran — the probe is cached per process, so the override would be ignored. Call _resetGamepadBackendCache() first (tests only).`);return c}return c=probeGamepadBackend(e),c}function reportGamepadBackendOnce(e){if(l)return;let t=e.shadow,r=t!==null&&t.status!==`sdl`?`[@gjsify/gamepad] ${n}=compare, but the SDL3 shim is ${t.status}: nothing to compare against. ${t.diagnostic??``}`.trim():null;if(e.status===`failed`){l=!0,console.error(e.diagnostic,e.error),r!==null&&warnWithCause(r,t?.error);return}e.diagnostic===null&&r===null||(l=!0,e.diagnostic!==null&&console.warn(e.diagnostic),r!==null&&warnWithCause(r,t?.error))}function warnWithCause(e,t){t==null?console.warn(e):console.warn(e,t)}function reportGamepadMonitorFault(e,t=null){let n=t?.name??`gamepad`,r=t?.startRequirements?`${t.startRequirements} `:``;console.error(`[@gjsify/gamepad] The ${n} backend loaded but the gamepad monitor could not be started — a fault AFTER the backend was available, not a missing backend. `+r+`getGamepads() reports no controllers until it is fixed.`,e)}async function hasGamepadBackend(){let{status:e}=await loadGamepadBackend();return e===`manette`||e===`sdl`}async function describeGamepadBackend(){let e=await loadGamepadBackend(),t={manette:`libmanette (gi://Manette)`,sdl:`SDL3 (gi://GjsifyGamepad)`};if(e.status===`absent`||e.status===`failed`)return`none (${e.status})`;let n=t[e.status];return e.shadow?.status===`sdl`?`${n}, compared against ${t.sdl}`:n}function _resetGamepadBackendCache(){c=null,l=!1}export{n as GAMEPAD_BACKEND_ENV,s as _GI_BACKENDS,_diagnoseGiLoadError,_resetGamepadBackendCache,describeGamepadBackend,hasGamepadBackend,loadGamepadBackend,reportGamepadBackendOnce,reportGamepadMonitorFault};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import"./axis-mapping.js";import"./button-mapping.js";const e=.02;var MirrorSink=class{_label;_report;_forward;devices=new Map;constructor(e,t,n){this._label=e,this._report=t,this._forward=n}connected(e){this.devices.has(e)||(this.devices.set(e,{device:e,buttons:Array.from({length:17}).fill(0),pressed:Array.from({length:17}).fill(!1),axes:Array.from({length:4}).fill(0)}),this._report(`${this._label}: connected "${e.id}"`)),this._forward?.connected(e)}disconnected(e){this.devices.delete(e)&&this._report(`${this._label}: disconnected "${e.id}"`),this._forward?.disconnected(e)}button(e,t,n,r){let i=this.devices.get(e);i&&t>=0&&t<17&&(i.buttons[t]=n,i.pressed[t]=r),this._forward?.button(e,t,n,r)}axis(e,t,n){let r=this.devices.get(e);r&&t>=0&&t<4&&(r.axes[t]=n),this._forward?.axis(e,t,n)}};function differences(t,n){let r=[];for(let i=0;i<17;i++){let a=t.buttons[i],o=n.buttons[i];(Math.abs(a-o)>e||t.pressed[i]!==n.pressed[i])&&r.push(`button ${i}: ${a.toFixed(2)}${t.pressed[i]?`*`:``} vs ${o.toFixed(2)}${n.pressed[i]?`*`:``}`)}for(let i=0;i<4;i++){let a=t.axes[i],o=n.axes[i];Math.abs(a-o)>e&&r.push(`axis ${i}: ${a.toFixed(2)} vs ${o.toFixed(2)}`)}return r}function pairUp(e,t){let n=[],r=[...t],i=[];for(let t of e){let e=t.device.model===void 0?-1:r.findIndex(e=>e.device.model===t.device.model);e>=0?n.push([t,r.splice(e,1)[0]]):i.push(t)}for(let e=0;e<Math.min(i.length,r.length);e++)n.push([i[e],r[e]]);return n}var ComparingSource=class{_primary;_shadow;_report;name;startRequirements;_primarySink=null;_shadowSink=null;_shadowStarted=!1;_pending=``;_reported=``;constructor(e,t,n=e=>console.warn(`[@gjsify/gamepad compare] ${e}`)){this._primary=e,this._shadow=t,this._report=n,this.name=`${e.name} (compared against ${t.name})`,this.startRequirements=e.startRequirements}start(e){this._primarySink=new MirrorSink(this._primary.name,this._report,e),this._shadowSink=new MirrorSink(this._shadow.name,this._report,null),this._primary.start(this._primarySink);try{this._shadow.start(this._shadowSink),this._shadowStarted=!0,this._report(`comparing ${this._primary.name} against ${this._shadow.name}`)}catch(e){this._report(`${this._shadow.name} did not start, nothing to compare against: ${String(e)}`),this._shadow.stop()}}poll(){this._primary.poll?.(),this._shadowStarted&&(this._shadow.poll?.(),this._compare())}stop(){this._primary.stop(),this._shadowStarted&&this._shadow.stop(),this._shadowStarted=!1,this._primarySink=null,this._shadowSink=null,this._pending=``,this._reported=``}_compare(){let e=[...this._primarySink?.devices.values()??[]],t=[...this._shadowSink?.devices.values()??[]],n=[];e.length!==t.length&&n.push(`${this._primary.name} sees ${e.length} controller(s), ${this._shadow.name} sees ${t.length}`);for(let[r,i]of pairUp(e,t)){let e=differences(r,i);e.length>0&&n.push(`"${r.device.id}" vs "${i.device.id}": ${e.join(`, `)}`)}let r=n.join(`
|
|
2
|
+
`),i=r===this._pending;if(this._pending=r,!(!i||r===this._reported))if(this._reported=r,r===``)this._report(`the two backends agree again`);else for(let e of n)this._report(e)}};export{ComparingSource};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import"./axis-mapping.js";import{loadGamepadBackend as e,reportGamepadBackendOnce as t,reportGamepadMonitorFault as n}from"./backend.js";import"./button-mapping.js";import{ComparingSource as r}from"./compare-source.js";import{GamepadButton as i}from"./gamepad-button.js";import{GamepadEvent as a}from"./gamepad-event.js";import{Gamepad as o}from"./gamepad.js";import{ManetteSource as s}from"./manette-source.js";import{SdlSource as c}from"./sdl-source.js";var GamepadManager=class{_slots=[];_injectedSource;_source=null;_started=!1;_initPromise=null;_initialized=!1;_sink={connected:e=>this._onDeviceConnected(e),disconnected:e=>this._onDeviceDisconnected(e),button:(e,t,n,r)=>this._onButton(e,t,n,r),axis:(e,t,n)=>this._onAxis(e,t,n)};constructor(e={}){this._injectedSource=e.source??null}_ensureInit(){this._initialized||(this._initPromise||=this._init().catch(e=>{n(e,this._source),this._initialized=!0}))}async _init(){let n=this._injectedSource;if(n===null){let i=await e();if(t(i),i.status===`sdl`)n=new c(i.module);else if(i.status===`manette`)n=new s(i.module),i.shadow?.status===`sdl`&&(n=new r(n,new c(i.shadow.module)));else{this._initialized=!0;return}}this._source=n,n.start(this._sink),this._started=!0,this._initialized=!0}_onDeviceConnected(e){if(this._findState(e))return;let t=this._slots.indexOf(null);t===-1&&(t=this._slots.length,this._slots.push(null));let n={device:e,index:t,connected:!0,timestamp:performance.now(),buttons:new Float64Array(17),buttonsPressed:Array.from({length:17}).fill(!1),axes:new Float64Array(4)};this._slots[t]=n,globalThis.dispatchEvent?.(new a(`gamepadconnected`,{gamepad:this._createSnapshot(n)}))}_onDeviceDisconnected(e){let t=this._findState(e);if(!t)return;t.connected=!1;let n=this._createSnapshot(t);this._slots[t.index]=null,globalThis.dispatchEvent?.(new a(`gamepaddisconnected`,{gamepad:n}))}_onButton(e,t,n,r){let i=this._findState(e);!i||!Number.isInteger(t)||t<0||t>=17||(i.buttons[t]=n,i.buttonsPressed[t]=r,i.timestamp=performance.now())}_onAxis(e,t,n){let r=this._findState(e);!r||!Number.isInteger(t)||t<0||t>=4||(r.axes[t]=n,r.timestamp=performance.now())}_findState(e){for(let t of this._slots)if(t&&t.device===e)return t;return null}_createSnapshot(e){let t=[];for(let n=0;n<17;n++)t.push(new i(e.buttonsPressed[n],e.buttonsPressed[n]||e.buttons[n]>0,e.buttons[n]));return new o({id:e.device.id,index:e.index,connected:e.connected,timestamp:e.timestamp,mapping:`standard`,axes:Array.from(e.axes),buttons:t,vibrationActuator:e.device.vibrationActuator})}getGamepads(){return this._ensureInit(),this._started&&this._source?.poll?.(),this._slots.map(e=>e?this._createSnapshot(e):null)}dispose(){this._source?.stop(),this._source=null,this._started=!1,this._slots.length=0,this._initialized=!1,this._initPromise=null}};export{GamepadManager};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";var
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{DOMException as e}from"@gjsify/dom-events";const t=5e3;function toMotor(e){return Math.round(e*65535)}function isUnitMagnitude(e){return Number.isFinite(e)&&e>=0&&e<=1}function isMilliseconds(e){return Number.isFinite(e)&&e>=0}var RumbleHapticActuator=class{_playing=null;playEffect(n,r={}){let i=r.duration??0,a=r.startDelay??0,o={strong:r.strongMagnitude??0,weak:r.weakMagnitude??0,leftTrigger:r.leftTrigger??0,rightTrigger:r.rightTrigger??0};if(n!==`dual-rumble`&&n!==`trigger-rumble`)return Promise.reject(TypeError(`Unknown haptic effect type '${String(n)}'.`));if(!isMilliseconds(i)||!isMilliseconds(a)||i+a>5e3)return Promise.reject(TypeError(`A haptic effect lasts 0..${t} ms including its startDelay.`));if(!(n===`dual-rumble`?[o.strong,o.weak]:Object.values(o)).every(isUnitMagnitude))return Promise.reject(TypeError(`Haptic effect magnitudes are 0..1.`));if(this._preempt(),!this.effects.includes(n))return Promise.reject(new e(`This gamepad cannot play a '${n}' effect.`,`NotSupportedError`));let s={strong:toMotor(o.strong),weak:toMotor(o.weak),leftTrigger:toMotor(o.leftTrigger),rightTrigger:toMotor(o.rightTrigger)};return new Promise(e=>{let t={resolve:e,timer:null};this._playing=t;let finish=()=>{this._playing===t&&(this._playing=null,e(`complete`))},start=()=>{if(this._playing===t){if(i===0){finish();return}this._start(n,s,Math.round(i)),t.timer=setTimeout(finish,i)}};a>0?t.timer=setTimeout(start,a):start()})}reset(){return this._preempt(),Promise.resolve(`complete`)}_preempt(){let e=this._playing;e&&(this._playing=null,e.timer!==null&&clearTimeout(e.timer),this._stop(),e.resolve(`preempted`))}},ManetteHapticActuator=class extends RumbleHapticActuator{_device;effects;constructor(e){super(),this._device=e,this.effects=e.has_rumble()?[`dual-rumble`]:[]}_start(e,t,n){this._device.rumble(t.strong,t.weak,n)}_stop(){this._device.rumble(0,0,0)}},SdlHapticActuator=class extends RumbleHapticActuator{_device;effects;constructor(e){super(),this._device=e;let t=[];e.has_rumble()&&t.push(`dual-rumble`),e.has_trigger_rumble()&&t.push(`trigger-rumble`),this.effects=t}_start(e,t,n){e===`trigger-rumble`&&(this._device.rumble_triggers(t.leftTrigger,t.rightTrigger,n),!this.effects.includes(`dual-rumble`))||this._device.rumble(t.strong,t.weak,n)}_stop(){this.effects.includes(`dual-rumble`)&&this._device.rumble(0,0,0),this.effects.includes(`trigger-rumble`)&&this._device.rumble_triggers(0,0,0)}};export{t as MAX_HAPTIC_EFFECT_DURATION_MS,ManetteHapticActuator,SdlHapticActuator};
|
package/lib/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n,W3CAxis as r,W3C_AXIS_COUNT as i}from"./axis-mapping.js";import{hasGamepadBackend as
|
|
1
|
+
import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n,W3CAxis as r,W3C_AXIS_COUNT as i}from"./axis-mapping.js";import{describeGamepadBackend as a,hasGamepadBackend as o}from"./backend.js";import{LinuxButton as s,MANETTE_TO_W3C_BUTTON as c,W3CButton as l,W3C_BUTTON_COUNT as u}from"./button-mapping.js";import{GamepadButton as d}from"./gamepad-button.js";import{GamepadEvent as f}from"./gamepad-event.js";import{Gamepad as p}from"./gamepad.js";import{ManetteHapticActuator as m}from"./haptic-actuator.js";import{GamepadManager as h}from"./gamepad-manager.js";export{p as Gamepad,d as GamepadButton,f as GamepadEvent,h as GamepadManager,e as MANETTE_TO_W3C_AXIS,c as MANETTE_TO_W3C_BUTTON,t as ManetteAxis,s as ManetteButton,m as ManetteHapticActuator,n as TRIGGER_PRESS_THRESHOLD,r as W3CAxis,l as W3CButton,i as W3C_AXIS_COUNT,u as W3C_BUTTON_COUNT,a as describeGamepadBackend,o as hasGamepadBackend};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{MANETTE_TO_W3C_AXIS as e,ManetteAxis as t,TRIGGER_PRESS_THRESHOLD as n}from"./axis-mapping.js";import{MANETTE_TO_W3C_BUTTON as r,W3CButton as i}from"./button-mapping.js";import{ManetteHapticActuator as a}from"./haptic-actuator.js";import{modelFromGuid as o}from"./source.js";var ManetteSource=class{_Manette;name=`gi://Manette`;startRequirements=`Manette.Monitor needs udev and /dev/input access, which a sandbox can withhold (flatpak: --device=input).`;_monitor=null;_monitorSignalIds=[];_devices=new Map;constructor(e){this._Manette=e}start(e){let t=new this._Manette.Monitor;this._monitor=t;let n=t.iterate(),r=n.next();for(;r[0];){let t=r[1];t&&this._add(t,e),r=n.next()}this._monitorSignalIds.push(t.connect(`device-connected`,(t,n)=>{this._add(n,e)}),t.connect(`device-disconnected`,(t,n)=>{this._remove(n,e)}))}stop(){for(let[e,t]of this._devices)for(let n of t.signalIds)e.disconnect(n);if(this._devices.clear(),this._monitor){for(let e of this._monitorSignalIds)this._monitor.disconnect(e);this._monitor=null}this._monitorSignalIds=[]}_add(e,t){if(this._devices.has(e))return;let n={id:e.get_name()??`Gamepad (${e.get_guid()})`,vibrationActuator:new a(e),model:o(e.get_guid())},r={handle:n,signalIds:[]};this._devices.set(e,r),r.signalIds.push(e.connect(`button-press-event`,(e,r)=>{this._onButton(n,r,!0,t)}),e.connect(`button-release-event`,(e,r)=>{this._onButton(n,r,!1,t)}),e.connect(`absolute-axis-event`,(e,r)=>{this._onAxis(n,r,t)}),e.connect(`hat-axis-event`,(e,r)=>{this._onHat(n,r,t)}),e.connect(`disconnected`,()=>{this._remove(e,t)})),t.connected(n)}_remove(e,t){let n=this._devices.get(e);if(n){for(let t of n.signalIds)e.disconnect(t);this._devices.delete(e),t.disconnected(n.handle)}}_onButton(e,t,n,i){let[a,o]=t.get_button();if(!a)return;let s=r.get(o);s!==void 0&&i.button(e,s,+!!n,n)}_onAxis(r,a,o){let[s,c,l]=a.get_absolute();if(!s)return;let u=e.get(c);if(u!==void 0){o.axis(r,u,l);return}let d=c===t.LEFT_TRIGGER?i.LEFT_TRIGGER:c===t.RIGHT_TRIGGER?i.RIGHT_TRIGGER:void 0;if(d===void 0)return;let f=(l+1)/2;o.button(r,d,f,f>n)}_onHat(e,t,n){let[r,a,o]=t.get_hat();if(!r)return;let[s,c]=a===0?[i.DPAD_LEFT,i.DPAD_RIGHT]:a===1?[i.DPAD_UP,i.DPAD_DOWN]:[void 0,void 0];s===void 0||c===void 0||(n.button(e,s,+(o<0),o<0),n.button(e,c,+(o>0),o>0))}};export{ManetteSource};
|
package/lib/esm/register.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{GamepadEvent as e}from"./gamepad-event.js";import{GamepadManager as t}from"./gamepad-manager.js";const n=globalThis;if(
|
|
1
|
+
import{GamepadEvent as e}from"./gamepad-event.js";import{GamepadManager as t}from"./gamepad-manager.js";import"@gjsify/node-globals/register/navigator";const n=globalThis;if(typeof n.navigator.getGamepads!=`function`){let e=new t;n.navigator.getGamepads=()=>e.getGamepads()}n.GamepadEvent===void 0&&(n.GamepadEvent=e);
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{TRIGGER_PRESS_THRESHOLD as e}from"./axis-mapping.js";import{W3CButton as t}from"./button-mapping.js";import{SdlHapticActuator as n}from"./haptic-actuator.js";import{modelFromGuid as r}from"./source.js";function isPressed(n,r){return n===t.LEFT_TRIGGER||n===t.RIGHT_TRIGGER?r>e:r>0}var SdlSource=class{_GjsifyGamepad;name=`gi://GjsifyGamepad`;startRequirements=`SDL's gamepad subsystem has to start in this process; the error carries SDL's own reason.`;_monitor=null;_sink=null;_signalIds=[];_devices=new Map;constructor(e){this._GjsifyGamepad=e}start(e){let t=this._GjsifyGamepad.Monitor.new();this._monitor=t,this._sink=e,this._signalIds.push(t.connect(`device-added`,(t,n)=>this._add(n,e)),t.connect(`device-removed`,(t,n)=>this._remove(n,e))),t.update()}poll(){let e=this._monitor,t=this._sink;if(!(!e||!t)){e.update();for(let[e,n]of this._devices)this._report(e,n,t)}}stop(){let e=this._monitor;if(this._monitor=null,this._sink=null,e){for(let t of this._signalIds)e.disconnect(t);e.close()}this._signalIds=[],this._devices.clear()}_add(e,t){if(this._devices.has(e))return;let i={id:e.get_name(),vibrationActuator:new n(e),model:r(e.get_guid())},a={handle:i,buttons:Array.from({length:17}).fill(0),axes:Array.from({length:4}).fill(0)};this._devices.set(e,a),t.connected(i),this._report(e,a,t)}_remove(e,t){let n=this._devices.get(e);n&&(this._devices.delete(e),t.disconnected(n.handle))}_report(e,t,n){let r=e.get_buttons();for(let e=0;e<17;e++){let i=r[e]??0;i!==t.buttons[e]&&(t.buttons[e]=i,n.button(t.handle,e,i,isPressed(e,i)))}let i=e.get_axes();for(let e=0;e<4;e++){let r=i[e]??0;r!==t.axes[e]&&(t.axes[e]=r,n.axis(t.handle,e,r))}}};export{SdlSource};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";function modelFromGuid(e){if(!e||e.length<20)return;let le16=t=>e.slice(t+2,t+4)+e.slice(t,t+2),t=le16(8);return t===`0000`?void 0:`${t}:${le16(16)}`.toLowerCase()}export{modelFromGuid};
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The axis codes `Manette.Event.get_absolute()` reports: Linux `ABS_*` codes. A mapped
|
|
3
|
+
* device is translated by libmanette's own table (`manette-mapping.c`: `leftx`→ABS_X,
|
|
4
|
+
* `lefty`→ABS_Y, `rightx`→ABS_RX, `righty`→ABS_RY; the triggers become the BUTTONS
|
|
5
|
+
* BTN_TL2/BTN_TR2), an unmapped one passes its kernel codes through. Measured with a
|
|
6
|
+
* uinput pad under gi://Manette: the right stick arrives as 3 and 4.
|
|
7
|
+
*
|
|
8
|
+
* NOT SDL mapping indices. This table once said RIGHT_X 2 / RIGHT_Y 3 / LEFT_TRIGGER 4,
|
|
9
|
+
* which put the right stick's X on W3C axis 3 and its Y on the left trigger for every
|
|
10
|
+
* controller libmanette maps; an 8BitDo N30 Pro 2 compared against the SDL3 shim showed it.
|
|
4
11
|
*/
|
|
5
12
|
export declare const ManetteAxis: {
|
|
6
13
|
readonly LEFT_X: 0;
|
|
7
14
|
readonly LEFT_Y: 1;
|
|
8
|
-
readonly RIGHT_X:
|
|
9
|
-
readonly RIGHT_Y:
|
|
10
|
-
readonly LEFT_TRIGGER:
|
|
15
|
+
readonly RIGHT_X: 3;
|
|
16
|
+
readonly RIGHT_Y: 4;
|
|
17
|
+
readonly LEFT_TRIGGER: 2;
|
|
11
18
|
readonly RIGHT_TRIGGER: 5;
|
|
12
19
|
};
|
|
13
20
|
/**
|
|
@@ -23,8 +30,8 @@ export declare const W3CAxis: {
|
|
|
23
30
|
/** Total number of axes in the W3C standard mapping. */
|
|
24
31
|
export declare const W3C_AXIS_COUNT = 4;
|
|
25
32
|
/**
|
|
26
|
-
* Maps
|
|
27
|
-
*
|
|
33
|
+
* Maps the libmanette axis code → W3C axis index.
|
|
34
|
+
* Only the four stick axes are in this map; the trigger codes are NOT —
|
|
28
35
|
* they are handled separately in gamepad-manager as buttons[6]/buttons[7].
|
|
29
36
|
*/
|
|
30
37
|
export declare const MANETTE_TO_W3C_AXIS: ReadonlyMap<number, number>;
|
package/lib/types/backend.d.ts
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
import type Manette from '@girs/manette-0.2';
|
|
2
|
+
import { type TargetOs } from '@gjsify/utils/core';
|
|
3
|
+
import type { GjsifyGamepadNamespace } from './sdl-namespace.js';
|
|
4
|
+
import type { GamepadSource } from './source.js';
|
|
2
5
|
/**
|
|
3
6
|
* What this host has.
|
|
4
7
|
*
|
|
5
8
|
* - `manette` — the Manette-0.2 typelib loaded and exposes `Monitor`.
|
|
6
|
-
* - `
|
|
7
|
-
*
|
|
9
|
+
* - `sdl` — the GjsifyGamepad-1.0 typelib (`@gjsify/gamepad-native`) loaded and
|
|
10
|
+
* exposes `Monitor`.
|
|
11
|
+
* - `absent` — no backend reachable from here: no libmanette, no gamepad-native
|
|
12
|
+
* prebuild, a `--app node` process without `@gjsify/node-gi`, or a target where
|
|
13
|
+
* `gi://` is stubbed by design.
|
|
8
14
|
* - `failed` — a backend should have been reachable and was not. A fault.
|
|
9
15
|
*/
|
|
10
|
-
export type GamepadBackendStatus = 'manette' | 'absent' | 'failed';
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export type GamepadBackendStatus = 'manette' | 'sdl' | 'absent' | 'failed';
|
|
17
|
+
/**
|
|
18
|
+
* Which backend a Linux process uses — `GJSIFY_GAMEPAD_BACKEND`, read once per process.
|
|
19
|
+
* darwin and win32 have only the SDL3 shim and ignore it.
|
|
20
|
+
*
|
|
21
|
+
* - `manette` (the default) — libmanette, as before Amendment 1.
|
|
22
|
+
* - `sdl` — the SDL3 shim alone.
|
|
23
|
+
* - `compare` — libmanette drives the page and the SDL3 shim runs beside it on the
|
|
24
|
+
* same controllers; every disagreement is written to stderr (`compare-source.ts`).
|
|
25
|
+
* This is the measurement Amendment 1 requires before libmanette is removed.
|
|
26
|
+
*/
|
|
27
|
+
export type GamepadBackendChoice = 'manette' | 'sdl' | 'compare';
|
|
28
|
+
/** The environment variable behind {@link GamepadBackendChoice}. */
|
|
29
|
+
export declare const GAMEPAD_BACKEND_ENV = "GJSIFY_GAMEPAD_BACKEND";
|
|
30
|
+
/** What every probe result carries besides the module. */
|
|
31
|
+
interface GamepadBackendReport {
|
|
15
32
|
/** The original load error for `absent`/`failed`; `null` otherwise. */
|
|
16
33
|
error: unknown;
|
|
17
34
|
/**
|
|
18
35
|
* The one-time line the USE site emits, or `null` when there is nothing to say.
|
|
19
|
-
* Carried rather than printed so the capability query stays silent.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* {@link isEmptiedGiModule}); `'failed'` always prints, with the original error, and
|
|
23
|
-
* needs no text here.
|
|
36
|
+
* Carried rather than printed so the capability query stays silent. `'absent'`
|
|
37
|
+
* with a `null` diagnostic is the by-design `gi://` stub (see
|
|
38
|
+
* {@link isEmptiedGiModule}).
|
|
24
39
|
*/
|
|
25
40
|
diagnostic: string | null;
|
|
41
|
+
/**
|
|
42
|
+
* `compare` mode only: the SDL3 shim's own probe, run beside the primary. The
|
|
43
|
+
* manager compares against it when it is `sdl` and reports its diagnostic when it
|
|
44
|
+
* is not. `null` in every other mode.
|
|
45
|
+
*/
|
|
46
|
+
shadow: GamepadBackend | null;
|
|
26
47
|
}
|
|
48
|
+
/** The resolved backend probe: a module exactly for the two usable states. */
|
|
49
|
+
export type GamepadBackend = GamepadBackendReport & ({
|
|
50
|
+
status: 'manette';
|
|
51
|
+
module: typeof Manette;
|
|
52
|
+
} | {
|
|
53
|
+
status: 'sdl';
|
|
54
|
+
module: GjsifyGamepadNamespace;
|
|
55
|
+
} | {
|
|
56
|
+
status: 'absent' | 'failed';
|
|
57
|
+
module: null;
|
|
58
|
+
});
|
|
27
59
|
export interface LoadGamepadBackendOptions {
|
|
28
60
|
/**
|
|
29
61
|
* Override the `gi://Manette` import. Tests only — the single seam that exercises the
|
|
@@ -33,19 +65,49 @@ export interface LoadGamepadBackendOptions {
|
|
|
33
65
|
importer?: () => Promise<{
|
|
34
66
|
default: typeof Manette;
|
|
35
67
|
}>;
|
|
68
|
+
/** Override the darwin branch's `gi://GjsifyGamepad` import. Tests only. */
|
|
69
|
+
sdlImporter?: () => Promise<{
|
|
70
|
+
default: GjsifyGamepadNamespace;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* Override the host OS the probe branches on. Tests only — it is how the darwin
|
|
74
|
+
* branch is exercised on a Linux runner and the Manette branch on a Mac.
|
|
75
|
+
*/
|
|
76
|
+
hostOs?: () => TargetOs | undefined;
|
|
77
|
+
/** Override the environment read for {@link GAMEPAD_BACKEND_ENV}. Tests only. */
|
|
78
|
+
env?: (name: string) => string | undefined;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One `gi://` backend: its namespace, which every wording is scoped to, and its three
|
|
82
|
+
* texts. Exported (underscored) for `backend.spec.ts`.
|
|
83
|
+
*/
|
|
84
|
+
export interface _GiBackend {
|
|
85
|
+
status: 'manette' | 'sdl';
|
|
86
|
+
namespace: string;
|
|
87
|
+
/** No typelib for {@link namespace} on this host: what to install. */
|
|
88
|
+
noTypelibText: string;
|
|
89
|
+
/** No `@gjsify/node-gi` in a `--app node` process: what to install. */
|
|
90
|
+
noBridgeText: string;
|
|
91
|
+
/** The load is a fault: said at `error` level, next to the original error. */
|
|
92
|
+
loadFaultText: string;
|
|
36
93
|
}
|
|
37
94
|
/** Absent-vs-fault plus the text the use site should print (`null` = silence). */
|
|
38
95
|
interface GiLoadDiagnosis {
|
|
39
96
|
status: 'absent' | 'failed';
|
|
40
97
|
diagnostic: string | null;
|
|
41
98
|
}
|
|
99
|
+
/** Both backends, for `backend.spec.ts`; the probe itself picks one by host OS. */
|
|
100
|
+
export declare const _GI_BACKENDS: {
|
|
101
|
+
readonly manette: _GiBackend;
|
|
102
|
+
readonly sdl: _GiBackend;
|
|
103
|
+
};
|
|
42
104
|
/**
|
|
43
105
|
* Classify a `gi://` load failure and pick the text for it. Exported (underscored) for
|
|
44
106
|
* `backend.spec.ts`, which pins the absent wording against the LIVE loader — hence the
|
|
45
|
-
*
|
|
107
|
+
* backend parameter: the test asks for a namespace that cannot exist and classifies the
|
|
46
108
|
* error the running GJS/girepository produced for it.
|
|
47
109
|
*/
|
|
48
|
-
export declare function _diagnoseGiLoadError(error: unknown,
|
|
110
|
+
export declare function _diagnoseGiLoadError(error: unknown, backend?: _GiBackend): GiLoadDiagnosis;
|
|
49
111
|
/**
|
|
50
112
|
* Resolve (once) which gamepad backend this host has. Emits NOTHING.
|
|
51
113
|
*
|
|
@@ -68,13 +130,16 @@ export declare function loadGamepadBackend(options?: LoadGamepadBackendOptions):
|
|
|
68
130
|
*/
|
|
69
131
|
export declare function reportGamepadBackendOnce(backend: GamepadBackend): void;
|
|
70
132
|
/**
|
|
71
|
-
* The backend loaded and then
|
|
72
|
-
* failed load: everything after the probe (`new Manette.Monitor()`, the device
|
|
73
|
-
* `connect()`) needs udev and `/dev/input`, which a sandbox can withhold from a
|
|
74
|
-
* whose typelib and shared library are both fine. Routing it through the
|
|
75
|
-
* would re-create the same conflation one layer up.
|
|
133
|
+
* The backend loaded and then its device source did not come up — a DIFFERENT failure
|
|
134
|
+
* from a failed load: everything after the probe (`new Manette.Monitor()`, the device
|
|
135
|
+
* walk, `connect()`) needs udev and `/dev/input`, which a sandbox can withhold from a
|
|
136
|
+
* process whose typelib and shared library are both fine. Routing it through the
|
|
137
|
+
* load-failure text would re-create the same conflation one layer up.
|
|
138
|
+
*
|
|
139
|
+
* The advice comes from the SOURCE (`startRequirements`), because it is per backend:
|
|
140
|
+
* udev is the right hint for Manette and the wrong one for anything else.
|
|
76
141
|
*/
|
|
77
|
-
export declare function reportGamepadMonitorFault(error: unknown): void;
|
|
142
|
+
export declare function reportGamepadMonitorFault(error: unknown, source?: GamepadSource | null): void;
|
|
78
143
|
/**
|
|
79
144
|
* Whether this host has a gamepad backend at all.
|
|
80
145
|
*
|
|
@@ -82,12 +147,17 @@ export declare function reportGamepadMonitorFault(error: unknown): void;
|
|
|
82
147
|
* what is connected — something the W3C surface itself cannot express, since it returns
|
|
83
148
|
* the same list either way. Answerable without constructing a monitor, and QUIET.
|
|
84
149
|
*
|
|
85
|
-
* `false` on
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
150
|
+
* `false` on Linux without libmanette (or, with `GJSIFY_GAMEPAD_BACKEND=sdl`, without the
|
|
151
|
+
* shim), and on macOS and Windows without the `@gjsify/gamepad-native` prebuild. Reports that the BRIDGE is usable, not that every
|
|
152
|
+
* later call succeeds — a monitor can still fail to start (see
|
|
153
|
+
* {@link reportGamepadMonitorFault}).
|
|
89
154
|
*/
|
|
90
155
|
export declare function hasGamepadBackend(): Promise<boolean>;
|
|
156
|
+
/**
|
|
157
|
+
* Which backend this process drives, in words — for a debug panel or a bug report, not
|
|
158
|
+
* for branching (use {@link hasGamepadBackend} for that). Quiet like the capability query.
|
|
159
|
+
*/
|
|
160
|
+
export declare function describeGamepadBackend(): Promise<string>;
|
|
91
161
|
/** Reset the cached probe and its one-time diagnostic — tests only. */
|
|
92
162
|
export declare function _resetGamepadBackendCache(): void;
|
|
93
163
|
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GamepadSource, GamepadSourceSink } from './source.js';
|
|
2
|
+
/** Where a comparison is written. `console.warn` unless a test captures it. */
|
|
3
|
+
export type CompareReport = (line: string) => void;
|
|
4
|
+
/**
|
|
5
|
+
* The primary drives the page; the shadow is compared against it.
|
|
6
|
+
*
|
|
7
|
+
* Devices are paired by model, then by connection order ({@link pairUp}). A count
|
|
8
|
+
* mismatch is itself reported.
|
|
9
|
+
*
|
|
10
|
+
* A difference is reported when it has held for two consecutive polls, and again
|
|
11
|
+
* only when the set of differences changes. Manette pushes from the main loop while
|
|
12
|
+
* SDL is pulled at the poll, so for one frame after a change the two legitimately
|
|
13
|
+
* disagree; a comparison that reported that would bury the real findings.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ComparingSource implements GamepadSource {
|
|
16
|
+
private readonly _primary;
|
|
17
|
+
private readonly _shadow;
|
|
18
|
+
private readonly _report;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly startRequirements?: string;
|
|
21
|
+
private _primarySink;
|
|
22
|
+
private _shadowSink;
|
|
23
|
+
private _shadowStarted;
|
|
24
|
+
private _pending;
|
|
25
|
+
private _reported;
|
|
26
|
+
constructor(_primary: GamepadSource, _shadow: GamepadSource, _report?: CompareReport);
|
|
27
|
+
start(sink: GamepadSourceSink): void;
|
|
28
|
+
poll(): void;
|
|
29
|
+
stop(): void;
|
|
30
|
+
private _compare;
|
|
31
|
+
}
|
|
@@ -1,31 +1,41 @@
|
|
|
1
1
|
import { Gamepad } from './gamepad.js';
|
|
2
|
+
import type { GamepadSource } from './source.js';
|
|
3
|
+
export interface GamepadManagerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Drive this source instead of probing the host's backend. For tests — it is how the
|
|
6
|
+
* manager is exercised with no controller and no typelib — and for an embedder that
|
|
7
|
+
* already owns an input stack.
|
|
8
|
+
*/
|
|
9
|
+
source?: GamepadSource;
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* snapshot, matching the W3C polling model.
|
|
12
|
+
* The W3C polling model over a {@link GamepadSource}: the source reports changes as they
|
|
13
|
+
* happen, this caches the latest state, and `getGamepads()` returns a snapshot.
|
|
6
14
|
*/
|
|
7
15
|
export declare class GamepadManager {
|
|
8
|
-
private _monitor;
|
|
9
16
|
/**
|
|
10
17
|
* The spec's `Navigator.[[gamepads]]` — an EMPTY list until something
|
|
11
18
|
* connects, then one entry per index ever handed out, `null` where the slot
|
|
12
19
|
* is free. Never pre-filled: see the note on {@link getGamepads}.
|
|
13
20
|
*/
|
|
14
21
|
private _slots;
|
|
15
|
-
private
|
|
16
|
-
private
|
|
22
|
+
private readonly _injectedSource;
|
|
23
|
+
private _source;
|
|
24
|
+
/** Whether `_source.start()` RETURNED — a source whose start threw is never polled. */
|
|
25
|
+
private _started;
|
|
17
26
|
private _initPromise;
|
|
18
27
|
private _initialized;
|
|
19
|
-
/**
|
|
28
|
+
/** The one sink every source reports through. */
|
|
29
|
+
private readonly _sink;
|
|
30
|
+
constructor(options?: GamepadManagerOptions);
|
|
31
|
+
/** Lazily start the device source, on the first `getGamepads()`. */
|
|
20
32
|
private _ensureInit;
|
|
21
33
|
private _init;
|
|
22
34
|
private _onDeviceConnected;
|
|
23
35
|
private _onDeviceDisconnected;
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private _onHatChange;
|
|
28
|
-
private _findStateByDevice;
|
|
36
|
+
private _onButton;
|
|
37
|
+
private _onAxis;
|
|
38
|
+
private _findState;
|
|
29
39
|
private _createSnapshot;
|
|
30
40
|
/**
|
|
31
41
|
* Returns a snapshot list matching the W3C `navigator.getGamepads()`
|
|
@@ -72,6 +82,6 @@ export declare class GamepadManager {
|
|
|
72
82
|
* plus the one-time diagnostic this manager emits on its first init.
|
|
73
83
|
*/
|
|
74
84
|
getGamepads(): (Gamepad | null)[];
|
|
75
|
-
/**
|
|
85
|
+
/** Stop the source and drop every index. */
|
|
76
86
|
dispose(): void;
|
|
77
87
|
}
|
package/lib/types/gamepad.d.ts
CHANGED
|
@@ -42,4 +42,8 @@ export interface GamepadEffectParameters {
|
|
|
42
42
|
startDelay?: number;
|
|
43
43
|
strongMagnitude?: number;
|
|
44
44
|
weakMagnitude?: number;
|
|
45
|
+
/** `trigger-rumble` only: the left trigger's motor, 0..1. */
|
|
46
|
+
leftTrigger?: number;
|
|
47
|
+
/** `trigger-rumble` only: the right trigger's motor, 0..1. */
|
|
48
|
+
rightTrigger?: number;
|
|
45
49
|
}
|
|
@@ -1,16 +1,67 @@
|
|
|
1
1
|
import type Manette from '@girs/manette-0.2';
|
|
2
|
-
import type { GamepadHapticActuator, GamepadHapticEffectType,
|
|
2
|
+
import type { GamepadEffectParameters, GamepadHapticActuator, GamepadHapticEffectType, GamepadHapticsResult } from './gamepad.js';
|
|
3
|
+
import type { GjsifyGamepadDevice } from './sdl-namespace.js';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* The longest effect (`startDelay + duration`) accepted, in ms — the spec's recommended
|
|
6
|
+
* maximum, and what Chromium enforces. It is also below both backends' own cap (SDL:
|
|
7
|
+
* 0xFFFF ms; libmanette: a signed 16-bit ms count), so a valid effect is never cut short.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_HAPTIC_EFFECT_DURATION_MS = 5000;
|
|
10
|
+
/** The motor intensities one effect asks for, each a uint16 both backends take. */
|
|
11
|
+
interface HapticMotors {
|
|
12
|
+
strong: number;
|
|
13
|
+
weak: number;
|
|
14
|
+
leftTrigger: number;
|
|
15
|
+
rightTrigger: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The W3C `playEffect()` / `reset()` algorithm, shared by every device source so a page
|
|
19
|
+
* behaves the same whichever backend its host has. A subclass only says which effects its
|
|
20
|
+
* device has and how to start and stop the motors.
|
|
5
21
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
22
|
+
* What the spec asks for, and why each step is here rather than left to the device:
|
|
23
|
+
* - parameters default to 0 (the IDL's defaults) and are validated first: a magnitude
|
|
24
|
+
* outside 0..1 or an effect longer than {@link MAX_HAPTIC_EFFECT_DURATION_MS} rejects
|
|
25
|
+
* with a `TypeError` instead of being clamped into something the page did not ask for;
|
|
26
|
+
* - an effect the device cannot play rejects with `NotSupportedError` — resolving it
|
|
27
|
+
* `'complete'` would tell the page it rumbled when nothing moved;
|
|
28
|
+
* - the promise resolves when the effect has PLAYED (`startDelay + duration`), or
|
|
29
|
+
* `'preempted'` when a later `playEffect()` / `reset()` interrupts it — which is how a
|
|
30
|
+
* page sequences effects, and why resolving at once would break it.
|
|
9
31
|
*/
|
|
10
|
-
|
|
11
|
-
readonly effects: readonly GamepadHapticEffectType[];
|
|
12
|
-
private
|
|
13
|
-
|
|
32
|
+
declare abstract class RumbleHapticActuator implements GamepadHapticActuator {
|
|
33
|
+
abstract readonly effects: readonly GamepadHapticEffectType[];
|
|
34
|
+
private _playing;
|
|
35
|
+
/** Start the motors for `durationMs`; the device stops them on its own afterwards. */
|
|
36
|
+
protected abstract _start(type: GamepadHapticEffectType, motors: HapticMotors, durationMs: number): void;
|
|
37
|
+
/** Stop every motor now. */
|
|
38
|
+
protected abstract _stop(): void;
|
|
14
39
|
playEffect(type: GamepadHapticEffectType, params?: GamepadEffectParameters): Promise<GamepadHapticsResult>;
|
|
15
40
|
reset(): Promise<GamepadHapticsResult>;
|
|
41
|
+
/** Stop the effect in flight, if any, and resolve its promise `'preempted'`. */
|
|
42
|
+
private _preempt;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* libmanette's rumble as a W3C GamepadHapticActuator: `dual-rumble` only, via
|
|
46
|
+
* `Device.rumble(strong_magnitude, weak_magnitude, milliseconds)`.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ManetteHapticActuator extends RumbleHapticActuator {
|
|
49
|
+
private readonly _device;
|
|
50
|
+
readonly effects: readonly GamepadHapticEffectType[];
|
|
51
|
+
constructor(_device: Manette.Device);
|
|
52
|
+
protected _start(_type: GamepadHapticEffectType, motors: HapticMotors, durationMs: number): void;
|
|
53
|
+
protected _stop(): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The SDL3 shim's rumble as a W3C GamepadHapticActuator: `dual-rumble` drives the two
|
|
57
|
+
* body motors; `trigger-rumble` drives the impulse triggers (Xbox One/Series controllers)
|
|
58
|
+
* AND the body motors, since its parameters carry both.
|
|
59
|
+
*/
|
|
60
|
+
export declare class SdlHapticActuator extends RumbleHapticActuator {
|
|
61
|
+
private readonly _device;
|
|
62
|
+
readonly effects: readonly GamepadHapticEffectType[];
|
|
63
|
+
constructor(_device: GjsifyGamepadDevice);
|
|
64
|
+
protected _start(type: GamepadHapticEffectType, motors: HapticMotors, durationMs: number): void;
|
|
65
|
+
protected _stop(): void;
|
|
16
66
|
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export type { GamepadMappingType, GamepadHapticActuator, GamepadHapticEffectType
|
|
|
4
4
|
export { GamepadEvent } from './gamepad-event.js';
|
|
5
5
|
export type { GamepadEventInit } from './gamepad-event.js';
|
|
6
6
|
export { GamepadManager } from './gamepad-manager.js';
|
|
7
|
-
export {
|
|
8
|
-
export type {
|
|
7
|
+
export type { GamepadManagerOptions } from './gamepad-manager.js';
|
|
8
|
+
export type { GamepadSource, GamepadSourceDevice, GamepadSourceSink } from './source.js';
|
|
9
|
+
export { describeGamepadBackend, hasGamepadBackend } from './backend.js';
|
|
10
|
+
export type { GamepadBackendChoice, GamepadBackendStatus } from './backend.js';
|
|
9
11
|
export { ManetteHapticActuator } from './haptic-actuator.js';
|
|
10
12
|
export { MANETTE_TO_W3C_BUTTON, ManetteButton, W3CButton, W3C_BUTTON_COUNT } from './button-mapping.js';
|
|
11
13
|
export { MANETTE_TO_W3C_AXIS, ManetteAxis, W3CAxis, W3C_AXIS_COUNT, TRIGGER_PRESS_THRESHOLD } from './axis-mapping.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type Manette from '@girs/manette-0.2';
|
|
2
|
+
import { type GamepadSource, type GamepadSourceSink } from './source.js';
|
|
3
|
+
/** `Manette.Monitor` as a {@link GamepadSource}. Push model: no `poll()`. */
|
|
4
|
+
export declare class ManetteSource implements GamepadSource {
|
|
5
|
+
private readonly _Manette;
|
|
6
|
+
readonly name = "gi://Manette";
|
|
7
|
+
readonly startRequirements = "Manette.Monitor needs udev and /dev/input access, which a sandbox can withhold (flatpak: --device=input).";
|
|
8
|
+
private _monitor;
|
|
9
|
+
private _monitorSignalIds;
|
|
10
|
+
private readonly _devices;
|
|
11
|
+
constructor(_Manette: typeof Manette);
|
|
12
|
+
start(sink: GamepadSourceSink): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
private _add;
|
|
15
|
+
private _remove;
|
|
16
|
+
private _onButton;
|
|
17
|
+
private _onAxis;
|
|
18
|
+
private _onHat;
|
|
19
|
+
}
|
package/lib/types/register.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import '@gjsify/node-globals/register/navigator';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** One controller: `GjsifyGamepad.Device`. */
|
|
2
|
+
export interface GjsifyGamepadDevice {
|
|
3
|
+
get_name(): string;
|
|
4
|
+
/** SDL's 32-hex-digit GUID; carries the USB vendor and product. */
|
|
5
|
+
get_guid(): string;
|
|
6
|
+
/** W3C standard buttons 0–16, 0..1 (the triggers 6/7 analog, the rest 0 or 1). */
|
|
7
|
+
get_buttons(): number[];
|
|
8
|
+
/** W3C standard axes 0–3, -1..1. */
|
|
9
|
+
get_axes(): number[];
|
|
10
|
+
has_rumble(): boolean;
|
|
11
|
+
has_trigger_rumble(): boolean;
|
|
12
|
+
rumble(lowFrequency: number, highFrequency: number, durationMs: number): boolean;
|
|
13
|
+
rumble_triggers(left: number, right: number, durationMs: number): boolean;
|
|
14
|
+
}
|
|
15
|
+
/** `GjsifyGamepad.Monitor` — the subset the source uses. */
|
|
16
|
+
export interface GjsifyGamepadMonitor {
|
|
17
|
+
update(): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
connect(signal: 'device-added' | 'device-removed', callback: (monitor: GjsifyGamepadMonitor, device: GjsifyGamepadDevice) => void): number;
|
|
20
|
+
disconnect(id: number): void;
|
|
21
|
+
}
|
|
22
|
+
/** The `gi://GjsifyGamepad` namespace — the subset the source uses. */
|
|
23
|
+
export interface GjsifyGamepadNamespace {
|
|
24
|
+
/** A GI class: the probe checks `typeof Monitor === 'function'`. */
|
|
25
|
+
Monitor: {
|
|
26
|
+
new: () => GjsifyGamepadMonitor;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GjsifyGamepadNamespace } from './sdl-namespace.js';
|
|
2
|
+
import { type GamepadSource, type GamepadSourceSink } from './source.js';
|
|
3
|
+
/**
|
|
4
|
+
* `GjsifyGamepad.Monitor` as a {@link GamepadSource}. Pull model: `poll()` pumps SDL and
|
|
5
|
+
* forwards the changed values, at exactly the W3C polling moment. Hotplug is also pushed
|
|
6
|
+
* between polls, by the monitor's own GLib timeout, so `gamepadconnected` reaches a page
|
|
7
|
+
* that has not polled yet.
|
|
8
|
+
*/
|
|
9
|
+
export declare class SdlSource implements GamepadSource {
|
|
10
|
+
private readonly _GjsifyGamepad;
|
|
11
|
+
readonly name = "gi://GjsifyGamepad";
|
|
12
|
+
readonly startRequirements = "SDL's gamepad subsystem has to start in this process; the error carries SDL's own reason.";
|
|
13
|
+
private _monitor;
|
|
14
|
+
private _sink;
|
|
15
|
+
private _signalIds;
|
|
16
|
+
private readonly _devices;
|
|
17
|
+
constructor(_GjsifyGamepad: GjsifyGamepadNamespace);
|
|
18
|
+
start(sink: GamepadSourceSink): void;
|
|
19
|
+
poll(): void;
|
|
20
|
+
stop(): void;
|
|
21
|
+
private _add;
|
|
22
|
+
private _remove;
|
|
23
|
+
/**
|
|
24
|
+
* Forward the values that changed since the sink last heard. Unchanged values are
|
|
25
|
+
* not re-sent because every sink call stamps `Gamepad.timestamp`, which the W3C
|
|
26
|
+
* spec advances only when the device reported new data.
|
|
27
|
+
*/
|
|
28
|
+
private _report;
|
|
29
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { GamepadHapticActuator } from './gamepad.js';
|
|
2
|
+
/**
|
|
3
|
+
* `vvvv:pppp` from an SDL-style joystick GUID (libmanette and SDL both use that layout:
|
|
4
|
+
* little-endian 16-bit bus, CRC, vendor, 0, product, …), or `undefined` when the GUID
|
|
5
|
+
* carries no vendor (SDL then stores a name hash there instead).
|
|
6
|
+
*/
|
|
7
|
+
export declare function modelFromGuid(guid: string | null | undefined): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* One physical device, as a source reports it. The OBJECT is the identity: the manager
|
|
10
|
+
* keys its slot on it, so a source must hand the same object to every call about the
|
|
11
|
+
* same device.
|
|
12
|
+
*/
|
|
13
|
+
export interface GamepadSourceDevice {
|
|
14
|
+
/** Becomes `Gamepad.id`. */
|
|
15
|
+
readonly id: string;
|
|
16
|
+
/** Becomes `Gamepad.vibrationActuator`; `null` when the source cannot drive one. */
|
|
17
|
+
readonly vibrationActuator: GamepadHapticActuator | null;
|
|
18
|
+
/**
|
|
19
|
+
* The controller MODEL as `vvvv:pppp` (USB vendor and product, hex), read from the
|
|
20
|
+
* SDL-style GUID both libmanette and SDL report. Optional: only `ComparingSource`
|
|
21
|
+
* reads it, to pair one controller across two backends that name it differently.
|
|
22
|
+
*/
|
|
23
|
+
readonly model?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Where a source reports to, in the W3C STANDARD mapping's vocabulary
|
|
27
|
+
* (https://w3c.github.io/gamepad/#remapping): `index` is a standard button or axis
|
|
28
|
+
* index, not a platform code. Calls about a device that is not connected, or with an
|
|
29
|
+
* index outside the standard layout, are ignored — a source may forward what it does not
|
|
30
|
+
* recognise without a guard of its own.
|
|
31
|
+
*/
|
|
32
|
+
export interface GamepadSourceSink {
|
|
33
|
+
/** A device became available — selects an index for it and fires `gamepadconnected`. */
|
|
34
|
+
connected(device: GamepadSourceDevice): void;
|
|
35
|
+
/** The device went away — frees its index and fires `gamepaddisconnected`. */
|
|
36
|
+
disconnected(device: GamepadSourceDevice): void;
|
|
37
|
+
/**
|
|
38
|
+
* A button's state. `value` is 0..1; `pressed` is passed explicitly because only the
|
|
39
|
+
* source knows whether the control is digital (pressed ⇔ value 1) or analog, where
|
|
40
|
+
* the threshold is its call.
|
|
41
|
+
*/
|
|
42
|
+
button(device: GamepadSourceDevice, index: number, value: number, pressed: boolean): void;
|
|
43
|
+
/** A stick axis, -1..1. Analog triggers are BUTTONS 6/7 in the standard layout. */
|
|
44
|
+
axis(device: GamepadSourceDevice, index: number, value: number): void;
|
|
45
|
+
}
|
|
46
|
+
/** A platform's gamepad subsystem, behind the one contract the manager drives. */
|
|
47
|
+
export interface GamepadSource {
|
|
48
|
+
/** Names the backend in diagnostics, e.g. `gi://Manette`. */
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/**
|
|
51
|
+
* One sentence on what this backend needs to start, appended to the report when
|
|
52
|
+
* {@link start} throws. That report must not guess: udev advice for a darwin backend
|
|
53
|
+
* would be wrong advice.
|
|
54
|
+
*/
|
|
55
|
+
readonly startRequirements?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Begin reporting to `sink`. Devices that are already present are reported as
|
|
58
|
+
* `connected` too. May throw — the manager reports that as a monitor fault, distinct
|
|
59
|
+
* from a backend that failed to load. Called again after {@link stop} when a disposed
|
|
60
|
+
* manager is used again.
|
|
61
|
+
*/
|
|
62
|
+
start(sink: GamepadSourceSink): void;
|
|
63
|
+
/**
|
|
64
|
+
* Refresh device state. A PULL backend (SDL is polled) implements it; the manager
|
|
65
|
+
* calls it at the top of every `getGamepads()`, which is exactly the W3C polling
|
|
66
|
+
* moment. A PUSH backend (Manette emits signals) omits it.
|
|
67
|
+
*/
|
|
68
|
+
poll?(): void;
|
|
69
|
+
/** Stop reporting and release every native handle. Must be safe after a failed start. */
|
|
70
|
+
stop(): void;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/gamepad",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Gamepad Web API for GJS
|
|
3
|
+
"version": "0.54.0",
|
|
4
|
+
"description": "Gamepad Web API for GJS: SDL3 (via @gjsify/gamepad-native) on macOS and Windows, libmanette on Linux until SDL is proven there",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -25,35 +25,44 @@
|
|
|
25
25
|
"./globals.mjs"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs",
|
|
28
|
+
"clear": "gjsify clear lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.hardware-check.gjs.mjs",
|
|
29
29
|
"check": "gjsify tsc --noEmit",
|
|
30
30
|
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
31
|
-
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test
|
|
31
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test*.{mts,ts}'",
|
|
32
32
|
"build:types": "gjsify tsc",
|
|
33
33
|
"build:test": "gjsify run build:test:gjs",
|
|
34
34
|
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
35
35
|
"build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
|
|
36
36
|
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
|
|
37
|
-
"test:gjs": "gjsify run test.gjs.mjs"
|
|
37
|
+
"test:gjs": "gjsify run test.gjs.mjs",
|
|
38
|
+
"hardware-check": "gjsify build src/hardware-check.mts --app gjs --outfile test.hardware-check.gjs.mjs && gjsify run test.hardware-check.gjs.mjs",
|
|
39
|
+
"test:gjs-on-node": "gjsify build src/test.node-gi.mts --app node --outfile dist/test.node-gi.mjs && node dist/test.node-gi.mjs"
|
|
38
40
|
},
|
|
39
41
|
"keywords": [
|
|
40
42
|
"gjs",
|
|
41
43
|
"gamepad",
|
|
42
44
|
"libmanette",
|
|
45
|
+
"sdl3",
|
|
43
46
|
"controller"
|
|
44
47
|
],
|
|
45
48
|
"dependencies": {
|
|
46
|
-
"@gjsify/dom-events": "^0.
|
|
49
|
+
"@gjsify/dom-events": "^0.54.0",
|
|
50
|
+
"@gjsify/node-globals": "^0.54.0",
|
|
51
|
+
"@gjsify/utils": "^0.54.0"
|
|
47
52
|
},
|
|
48
53
|
"devDependencies": {
|
|
49
54
|
"@girs/gjs": "5.4.0",
|
|
50
55
|
"@girs/glib-2.0": "5.4.0",
|
|
51
56
|
"@girs/manette-0.2": "5.4.0",
|
|
52
|
-
"@gjsify/cli": "^0.
|
|
53
|
-
"@gjsify/
|
|
57
|
+
"@gjsify/cli": "^0.54.0",
|
|
58
|
+
"@gjsify/node-gi": "file:../../../packages/node-gi/node-gi",
|
|
59
|
+
"@gjsify/unit": "^0.54.0",
|
|
54
60
|
"@types/node": "^25.9.2",
|
|
55
61
|
"typescript": "^6.0.3"
|
|
56
62
|
},
|
|
63
|
+
"optionalDependencies": {
|
|
64
|
+
"@gjsify/gamepad-native": "^0.54.0"
|
|
65
|
+
},
|
|
57
66
|
"gjsify": {
|
|
58
67
|
"runtimes": {
|
|
59
68
|
"gjs": "polyfill",
|
|
@@ -61,6 +70,15 @@
|
|
|
61
70
|
"browser": "native",
|
|
62
71
|
"nativescript": "native"
|
|
63
72
|
},
|
|
73
|
+
"os": {
|
|
74
|
+
"linux": "supported",
|
|
75
|
+
"darwin": "partial",
|
|
76
|
+
"win32": "partial"
|
|
77
|
+
},
|
|
78
|
+
"osNotes": {
|
|
79
|
+
"darwin": "The backend is SDL3 behind a GObject shim (@gjsify/gamepad-native, ADR 0075). It builds, loads and enumerates on macOS, but input from a real controller has not been verified yet, and its darwin prebuilds are not published yet. Without the prebuild, getGamepads() answers the conformant empty list and hasGamepadBackend() reports false.",
|
|
80
|
+
"win32": "The backend is the SDL3 shim (@gjsify/gamepad-native-win32-x64, ADR 0075 Amendment 1), reached from Node through @gjsify/node-gi since Windows has no GJS. It builds, loads and enumerates in CI, and a virtual XInput pad is read there, but input from a real controller has not been verified yet and the win32 prebuild is not published yet. Without it, getGamepads() answers the conformant empty list and hasGamepadBackend() reports false."
|
|
81
|
+
},
|
|
64
82
|
"tier": 1
|
|
65
83
|
},
|
|
66
84
|
"license": "MIT",
|