@typeonce/ritmo-browser 0.10.0 → 0.11.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/CHANGELOG.md +83 -0
- package/README.md +6 -1
- package/dist/Controls.d.ts +495 -0
- package/dist/Controls.d.ts.map +1 -0
- package/dist/Controls.js +343 -0
- package/dist/Controls.js.map +1 -0
- package/dist/Gamepad.d.ts +375 -0
- package/dist/Gamepad.d.ts.map +1 -0
- package/dist/Gamepad.js +452 -0
- package/dist/Gamepad.js.map +1 -0
- package/dist/InputCapture.d.ts +46 -16
- package/dist/InputCapture.d.ts.map +1 -1
- package/dist/InputCapture.js +1 -1
- package/dist/InputCapture.js.map +1 -1
- package/dist/Keyboard.d.ts +184 -21
- package/dist/Keyboard.d.ts.map +1 -1
- package/dist/Keyboard.js +257 -91
- package/dist/Keyboard.js.map +1 -1
- package/dist/Pointer.d.ts +231 -62
- package/dist/Pointer.d.ts.map +1 -1
- package/dist/Pointer.js +205 -145
- package/dist/Pointer.js.map +1 -1
- package/dist/Touch.d.ts +73 -27
- package/dist/Touch.d.ts.map +1 -1
- package/dist/Touch.js +25 -30
- package/dist/Touch.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/buttons.d.ts +52 -0
- package/dist/internal/buttons.d.ts.map +1 -0
- package/dist/internal/buttons.js +211 -0
- package/dist/internal/buttons.js.map +1 -0
- package/dist/internal/introspect.d.ts +14 -0
- package/dist/internal/introspect.d.ts.map +1 -0
- package/dist/internal/introspect.js +19 -0
- package/dist/internal/introspect.js.map +1 -0
- package/dist/internal/used.d.ts +8 -0
- package/dist/internal/used.d.ts.map +1 -0
- package/dist/internal/used.js +14 -0
- package/dist/internal/used.js.map +1 -0
- package/dist/internal/vectors.d.ts +24 -0
- package/dist/internal/vectors.d.ts.map +1 -0
- package/dist/internal/vectors.js +22 -0
- package/dist/internal/vectors.js.map +1 -0
- package/package.json +4 -3
- package/src/Controls.ts +799 -0
- package/src/Gamepad.ts +747 -0
- package/src/InputCapture.ts +43 -24
- package/src/Keyboard.ts +395 -68
- package/src/Pointer.ts +392 -164
- package/src/Touch.ts +111 -54
- package/src/index.ts +10 -1
- package/src/internal/buttons.ts +222 -0
- package/src/internal/introspect.ts +23 -0
- package/src/internal/used.ts +14 -0
- package/src/internal/vectors.ts +36 -0
package/src/Gamepad.ts
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamepad input as caller-named actions, plus the sticks and triggers.
|
|
3
|
+
*
|
|
4
|
+
* Games bind actions to buttons once and read one snapshot per update, as
|
|
5
|
+
* with `Keyboard`. The module owns the plumbing every game rewrites: polling
|
|
6
|
+
* (the Gamepad API has no button events), press/release edges, a radial dead
|
|
7
|
+
* zone on the sticks, following the pad that was used last when several are
|
|
8
|
+
* connected, and releasing everything held when a pad disconnects. It does
|
|
9
|
+
* not know about the ECS; `InputCapture.system(...)` copies snapshots from a
|
|
10
|
+
* service into a resource.
|
|
11
|
+
*
|
|
12
|
+
* Buttons are named by their place in the browser's standard mapping
|
|
13
|
+
* (`"south"` is A on Xbox, Cross on PlayStation, B on Nintendo), so bindings
|
|
14
|
+
* stay the same on every pad. A pad whose `mapping` is not `"standard"` is
|
|
15
|
+
* read by the same indices, which may not match its labels: let players
|
|
16
|
+
* remap (`rebind`). The snapshot also reports every `button` by name,
|
|
17
|
+
* whatever the bindings, for fixed menu navigation and for a settings screen
|
|
18
|
+
* that waits for the button to bind; `parseRebinding(...)` validates
|
|
19
|
+
* bindings saved as JSON.
|
|
20
|
+
*
|
|
21
|
+
* The Gamepad API has no button events, so the pads are read on every
|
|
22
|
+
* animation frame and on every `snapshot()`; a tap that starts and ends
|
|
23
|
+
* between two updates still counts. Browsers expose a pad only after
|
|
24
|
+
* a button is pressed on it, and only in a secure context (HTTPS or
|
|
25
|
+
* localhost); elsewhere, or when a permission policy blocks the API, the
|
|
26
|
+
* live actions read no pad and report `connected: false`.
|
|
27
|
+
*
|
|
28
|
+
* `actions(...)` is one player on whichever pad is used. For local
|
|
29
|
+
* multiplayer, `pads(host, bindings, max)` reads every pad on its own for
|
|
30
|
+
* `Controls.players(...).pool(...)` (`scriptedPads(...)` in tests).
|
|
31
|
+
*
|
|
32
|
+
* @module Gamepad
|
|
33
|
+
* @docGroup browser
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const pad = Gamepad.actions(window, {
|
|
38
|
+
* move: Gamepad.vector({ stick: "left", up: ["dpadUp"], down: ["dpadDown"], left: ["dpadLeft"], right: ["dpadRight"] }),
|
|
39
|
+
* attack: ["west"],
|
|
40
|
+
* pause: ["start"]
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Once per update:
|
|
44
|
+
* const snapshot = pad.snapshot()
|
|
45
|
+
* move(snapshot.vectors.move) // the stick's tilt, or the d-pad's direction
|
|
46
|
+
* if (snapshot.actions.attack.pressed) attack()
|
|
47
|
+
*
|
|
48
|
+
* // In a settings screen, rebinding keeps the action names:
|
|
49
|
+
* pad.rebind({ move: Gamepad.vector({ stick: "right" }), attack: ["north"], pause: ["start"] })
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* For tests, replays, and headless runs, `scripted(...)` plays a timeline of
|
|
53
|
+
* button presses, releases, and stick moves instead of polling a host, and
|
|
54
|
+
* `recording(...)` wraps live actions to capture the timeline a session
|
|
55
|
+
* produced. `parseTimeline(...)` validates a timeline loaded from JSON.
|
|
56
|
+
*/
|
|
57
|
+
import type * as System from "@typeonce/ritmo/System"
|
|
58
|
+
import * as Result from "@typeonce/ritmo/Result"
|
|
59
|
+
|
|
60
|
+
import type * as BrowserTickSource from "./BrowserTickSource.ts"
|
|
61
|
+
import type { ActionState } from "./Keyboard.ts"
|
|
62
|
+
import * as ButtonsInternal from "./internal/buttons.ts"
|
|
63
|
+
import { buttonTracker, flattenButtons, idleActions, idleButtons, idleVectors, splitActions } from "./internal/buttons.ts"
|
|
64
|
+
import { digital, isVectorBinding, longer } from "./internal/vectors.ts"
|
|
65
|
+
import { anyUsed } from "./internal/used.ts"
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The buttons of the browser's standard mapping, in index order: face
|
|
69
|
+
* buttons by compass position, bumpers and triggers, the two small center
|
|
70
|
+
* buttons, the stick clicks, the d-pad, and the home button.
|
|
71
|
+
*/
|
|
72
|
+
export const buttonNames = [
|
|
73
|
+
"south",
|
|
74
|
+
"east",
|
|
75
|
+
"west",
|
|
76
|
+
"north",
|
|
77
|
+
"leftBumper",
|
|
78
|
+
"rightBumper",
|
|
79
|
+
"leftTrigger",
|
|
80
|
+
"rightTrigger",
|
|
81
|
+
"select",
|
|
82
|
+
"start",
|
|
83
|
+
"leftStick",
|
|
84
|
+
"rightStick",
|
|
85
|
+
"dpadUp",
|
|
86
|
+
"dpadDown",
|
|
87
|
+
"dpadLeft",
|
|
88
|
+
"dpadRight",
|
|
89
|
+
"home"
|
|
90
|
+
] as const
|
|
91
|
+
|
|
92
|
+
/** One button of the standard mapping, see {@link buttonNames}. */
|
|
93
|
+
export type Button = (typeof buttonNames)[number]
|
|
94
|
+
|
|
95
|
+
/** The buttons of one action: at least one. */
|
|
96
|
+
export type Buttons = readonly [Button, ...Array<Button>]
|
|
97
|
+
|
|
98
|
+
/** The two sticks a vector action can read. */
|
|
99
|
+
export type StickName = "left" | "right"
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A vector action (movement) on the pad, built with {@link vector}: a
|
|
103
|
+
* stick, buttons for the four directions (the d-pad), or both.
|
|
104
|
+
*/
|
|
105
|
+
export interface VectorBinding {
|
|
106
|
+
readonly _tag: "Vector"
|
|
107
|
+
readonly stick?: StickName
|
|
108
|
+
readonly up?: Buttons
|
|
109
|
+
readonly down?: Buttons
|
|
110
|
+
readonly left?: Buttons
|
|
111
|
+
readonly right?: Buttons
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Action name to the buttons that trigger it (at least one), or to a
|
|
116
|
+
* {@link vector} binding for a direction such as movement.
|
|
117
|
+
*/
|
|
118
|
+
export type Bindings = Readonly<Record<string, Buttons | VectorBinding>>
|
|
119
|
+
|
|
120
|
+
/** The bound button actions: the names bound to buttons. */
|
|
121
|
+
export type ButtonNames<B> = { [K in keyof B]: B[K] extends VectorBinding ? never : K }[keyof B] & string
|
|
122
|
+
|
|
123
|
+
/** The bound vector actions: the names bound with {@link vector}. */
|
|
124
|
+
export type VectorNames<B> = { [K in keyof B]: B[K] extends VectorBinding ? K : never }[keyof B] & string
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Binds a vector action to a stick, to buttons for its four directions, or
|
|
128
|
+
* to both. The snapshot's `vectors` reports the longer of the two: the
|
|
129
|
+
* stick keeps its tilt (after the dead zone), and the buttons give length 1
|
|
130
|
+
* (a diagonal is not faster).
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const bindings = {
|
|
135
|
+
* move: Gamepad.vector({ stick: "left", up: ["dpadUp"], down: ["dpadDown"], left: ["dpadLeft"], right: ["dpadRight"] }),
|
|
136
|
+
* jump: ["south"]
|
|
137
|
+
* } as const
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export const vector = (
|
|
141
|
+
binding:
|
|
142
|
+
| { readonly stick: StickName }
|
|
143
|
+
| { readonly stick?: StickName; readonly up: Buttons; readonly down: Buttons; readonly left: Buttons; readonly right: Buttons }
|
|
144
|
+
): VectorBinding => ({ _tag: "Vector", ...binding })
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* New buttons for the same actions, for {@link Actions.rebind}: buttons for
|
|
148
|
+
* a button action, a {@link vector} for a vector action. Load saved ones
|
|
149
|
+
* with {@link parseRebinding}.
|
|
150
|
+
*/
|
|
151
|
+
export type Rebinding<B extends Bindings> = { readonly [Action in keyof B]: B[Action] extends VectorBinding ? VectorBinding : Buttons }
|
|
152
|
+
|
|
153
|
+
/** One button's reading, the subset of the browser's `GamepadButton` the module reads. */
|
|
154
|
+
export interface ButtonReading {
|
|
155
|
+
readonly pressed: boolean
|
|
156
|
+
readonly value: number
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** One pad's reading, the subset of the browser's `Gamepad` the module reads. */
|
|
160
|
+
export interface PadReading {
|
|
161
|
+
readonly id: string
|
|
162
|
+
readonly index: number
|
|
163
|
+
readonly connected: boolean
|
|
164
|
+
readonly mapping: string
|
|
165
|
+
readonly buttons: ReadonlyArray<ButtonReading>
|
|
166
|
+
readonly axes: ReadonlyArray<number>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Where pads are read from, the subset of the browser's `navigator` the module reads. */
|
|
170
|
+
export interface GamepadNavigator {
|
|
171
|
+
getGamepads(): ReadonlyArray<PadReading | null>
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* What live actions need from the window: the pads, and animation frames to
|
|
176
|
+
* read them on, since the Gamepad API has no button events.
|
|
177
|
+
*/
|
|
178
|
+
export interface GamepadHost extends BrowserTickSource.Host {
|
|
179
|
+
readonly navigator: GamepadNavigator
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A direction and strength, length at most 1, `y` growing down as on
|
|
184
|
+
* screen: a stick's tilt (dead zone removed, zero at rest, 1 at full tilt)
|
|
185
|
+
* or a vector action's direction.
|
|
186
|
+
*/
|
|
187
|
+
export interface Vector {
|
|
188
|
+
readonly x: number
|
|
189
|
+
readonly y: number
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The followed pad's state, with the same core as the other devices'
|
|
194
|
+
* snapshots (`actions`, `vectors`, and `used`) so `Controls` can merge it.
|
|
195
|
+
* With no pad everything is idle and `connected` is `false`.
|
|
196
|
+
*/
|
|
197
|
+
export interface Snapshot<B extends Bindings> {
|
|
198
|
+
/** Every bound button action's state. */
|
|
199
|
+
readonly actions: { readonly [Action in ButtonNames<B>]: ActionState }
|
|
200
|
+
/** Each vector action's direction, length at most 1, `y` growing down. */
|
|
201
|
+
readonly vectors: { readonly [Action in VectorNames<B>]: Vector }
|
|
202
|
+
/** Every button's state by name, whatever the bindings. */
|
|
203
|
+
readonly buttons: { readonly [Name in Button]: ActionState }
|
|
204
|
+
/** The left stick's tilt, whatever the bindings. */
|
|
205
|
+
readonly left: Vector
|
|
206
|
+
/** The right stick's tilt, whatever the bindings. */
|
|
207
|
+
readonly right: Vector
|
|
208
|
+
/** How far the left trigger is pulled, 0 to 1. */
|
|
209
|
+
readonly leftTrigger: number
|
|
210
|
+
/** How far the right trigger is pulled, 0 to 1. */
|
|
211
|
+
readonly rightTrigger: number
|
|
212
|
+
/**
|
|
213
|
+
* Some button was pressed in this window or is held, or a stick is past
|
|
214
|
+
* the dead zone: the player is using the pad.
|
|
215
|
+
*/
|
|
216
|
+
readonly used: boolean
|
|
217
|
+
/** Whether a pad is being followed. */
|
|
218
|
+
readonly connected: boolean
|
|
219
|
+
/** The followed pad's `id` (it names the maker, for button labels), or `null`. */
|
|
220
|
+
readonly id: string | null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Rejects actions a rebinding names that the bindings do not have, so
|
|
225
|
+
* {@link Actions.rebind} takes exactly the actions it was made with.
|
|
226
|
+
*/
|
|
227
|
+
export type OnlyActionsOf<R, B extends Bindings> = {
|
|
228
|
+
readonly [Name in Exclude<keyof R, keyof B>]: `${Name & string} is not an action of these bindings`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A pad bound to actions, built with {@link actions} (live), {@link scripted},
|
|
233
|
+
* or {@link recording}. Capture it with `InputCapture.system(...)`, or merge
|
|
234
|
+
* it with `Controls`.
|
|
235
|
+
*/
|
|
236
|
+
export interface Actions<B extends Bindings> extends System.Consumes<"snapshot"> {
|
|
237
|
+
/** Returns the input state and starts a new press/release window. */
|
|
238
|
+
snapshot(): Snapshot<B>
|
|
239
|
+
/**
|
|
240
|
+
* Binds the same actions to new buttons, from the next snapshot, for a
|
|
241
|
+
* settings screen where players remap controls (capture the button to bind
|
|
242
|
+
* from the snapshot's `buttons`). An action held only through buttons it
|
|
243
|
+
* lost reports `released`. A button held across the rebind holds a newly
|
|
244
|
+
* bound action only once it is pressed again, so an action never turns
|
|
245
|
+
* held without a press. It takes exactly the actions the source was made
|
|
246
|
+
* with.
|
|
247
|
+
*/
|
|
248
|
+
rebind<const R extends Rebinding<B>>(bindings: R & OnlyActionsOf<R, B>): void
|
|
249
|
+
/** Stops reading pads and cancels the animation frame. */
|
|
250
|
+
dispose(): void
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Options for {@link actions} and {@link pads}. */
|
|
254
|
+
export interface ActionsOptions {
|
|
255
|
+
/** Fraction of a stick's tilt that reads as zero. Defaults to 0.15. */
|
|
256
|
+
readonly deadZone?: number
|
|
257
|
+
/** How far a trigger is pulled before it counts as held. Defaults to 0.5. */
|
|
258
|
+
readonly triggerThreshold?: number
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const zero: Vector = { x: 0, y: 0 }
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A snapshot with nothing held and no pad, for initializing an input
|
|
266
|
+
* resource before the first capture.
|
|
267
|
+
*/
|
|
268
|
+
export const idle = <const B extends Bindings>(bindings: B): Snapshot<B> => {
|
|
269
|
+
return { actions: idleActions(bindings), vectors: idleVectors(bindings) as Snapshot<B>["vectors"], buttons: idleButtons(buttonNames), left: zero, right: zero, leftTrigger: 0, rightTrigger: 0, used: false, connected: false, id: null }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sameVector = (a: Vector, b: Vector): boolean => a.x === b.x && a.y === b.y
|
|
273
|
+
|
|
274
|
+
/** Removes the dead zone and rescales, so the stick goes from zero to full tilt smoothly. */
|
|
275
|
+
const stick = (x: number, y: number, deadZone: number): Vector => {
|
|
276
|
+
const length = Math.sqrt(x * x + y * y)
|
|
277
|
+
if (length <= deadZone) return zero
|
|
278
|
+
const scaled = Math.min(1, (length - deadZone) / (1 - deadZone))
|
|
279
|
+
return { x: (x / length) * scaled, y: (y / length) * scaled }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The button tracker with the pad's analog state, shared by live and scripted actions. */
|
|
283
|
+
const tracker = <B extends Bindings>(initial: B) => {
|
|
284
|
+
// Vector actions are tracked as their four directions, next to the button actions.
|
|
285
|
+
let bindings: Bindings = initial
|
|
286
|
+
const buttons = buttonTracker(buttonNames, flattenButtons<Button>(initial))
|
|
287
|
+
return {
|
|
288
|
+
held: buttons.held,
|
|
289
|
+
down: buttons.down,
|
|
290
|
+
up: buttons.up,
|
|
291
|
+
upAll: buttons.upAll,
|
|
292
|
+
rebind(next: Rebinding<B>) {
|
|
293
|
+
bindings = next
|
|
294
|
+
buttons.rebind(flattenButtons<Button>(next))
|
|
295
|
+
},
|
|
296
|
+
snapshot(analog: Omit<Snapshot<B>, "actions" | "vectors" | "buttons" | "used">): Snapshot<B> {
|
|
297
|
+
const taken = buttons.take()
|
|
298
|
+
// The actions are fixed at creation; a rebind only changes how each is bound.
|
|
299
|
+
const { actions, held } = splitActions(initial, taken.actions)
|
|
300
|
+
const vectors: Record<string, Vector> = {}
|
|
301
|
+
for (const [name, entry] of Object.entries(initial)) {
|
|
302
|
+
if (!isVectorBinding(entry)) continue
|
|
303
|
+
const binding = bindings[name] as VectorBinding
|
|
304
|
+
const pad = digital(held(name, "up"), held(name, "down"), held(name, "left"), held(name, "right"))
|
|
305
|
+
vectors[name] = binding.stick === undefined ? pad : longer(analog[binding.stick], pad)
|
|
306
|
+
}
|
|
307
|
+
const used = anyUsed(taken.buttons) || !sameVector(analog.left, zero) || !sameVector(analog.right, zero)
|
|
308
|
+
return { actions, vectors, buttons: taken.buttons, ...analog, used } as Snapshot<B>
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Whether a pad is being touched: a button down or a stick past the dead zone. */
|
|
314
|
+
const inUse = (pad: PadReading, deadZone: number, triggerThreshold: number): boolean =>
|
|
315
|
+
pad.buttons.some((button, index) => isDown(button, index, triggerThreshold)) ||
|
|
316
|
+
// Only the sticks' axes: some pads report their triggers as extra axes resting at -1.
|
|
317
|
+
pad.axes.slice(0, 4).some((axis) => Math.abs(axis) > deadZone)
|
|
318
|
+
|
|
319
|
+
const triggerIndices = new Set([6, 7])
|
|
320
|
+
|
|
321
|
+
const isDown = (button: ButtonReading, index: number, triggerThreshold: number): boolean =>
|
|
322
|
+
triggerIndices.has(index) ? button.value >= triggerThreshold : button.pressed
|
|
323
|
+
|
|
324
|
+
/** Reads the pads, treating a host that throws (a blocked permission policy) as having none. */
|
|
325
|
+
const readPads = (host: GamepadHost): ReadonlyArray<PadReading | null> => {
|
|
326
|
+
try {
|
|
327
|
+
return host.navigator.getGamepads()
|
|
328
|
+
} catch {
|
|
329
|
+
return []
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Starts reading pads from `host`, usually `window`, on every animation
|
|
335
|
+
* frame until `dispose()`. With several pads connected it follows the one
|
|
336
|
+
* used last; switching pads, or the followed pad disconnecting, releases
|
|
337
|
+
* everything it held. Use {@link pads} to give each pad its own player.
|
|
338
|
+
*/
|
|
339
|
+
export const actions = <const B extends Bindings>(host: GamepadHost, bindings: B, options: ActionsOptions = {}): Actions<B> => {
|
|
340
|
+
const deadZone = options.deadZone ?? 0.15
|
|
341
|
+
const triggerThreshold = options.triggerThreshold ?? 0.5
|
|
342
|
+
const state = tracker(bindings)
|
|
343
|
+
let followed: number | null = null
|
|
344
|
+
let disposed = false
|
|
345
|
+
let reading: PadReading | null = null
|
|
346
|
+
let frame = 0
|
|
347
|
+
|
|
348
|
+
const poll = (): void => {
|
|
349
|
+
if (disposed) return
|
|
350
|
+
const pads = readPads(host).filter((pad): pad is PadReading => pad !== null && pad.connected)
|
|
351
|
+
const current = pads.find((pad) => pad.index === followed)
|
|
352
|
+
// Keep the followed pad while it is in use; otherwise switch to another one being used, or keep the first there is.
|
|
353
|
+
const used = current !== undefined && inUse(current, deadZone, triggerThreshold)
|
|
354
|
+
? current
|
|
355
|
+
: pads.find((pad) => pad.index !== followed && inUse(pad, deadZone, triggerThreshold))
|
|
356
|
+
const next = used ?? current ?? pads[0]
|
|
357
|
+
if (next?.index !== followed) state.upAll()
|
|
358
|
+
followed = next?.index ?? null
|
|
359
|
+
reading = next ?? null
|
|
360
|
+
if (next === undefined) return
|
|
361
|
+
readButtons(state, next, triggerThreshold)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const onFrame = (): void => {
|
|
365
|
+
if (disposed) return
|
|
366
|
+
poll()
|
|
367
|
+
frame = host.requestAnimationFrame(onFrame)
|
|
368
|
+
}
|
|
369
|
+
frame = host.requestAnimationFrame(onFrame)
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
snapshot() {
|
|
373
|
+
poll()
|
|
374
|
+
const pad = reading
|
|
375
|
+
if (pad === null) return state.snapshot({ ...resting, connected: false, id: null })
|
|
376
|
+
return state.snapshot({ ...analogOf(pad, deadZone), connected: true, id: pad.id })
|
|
377
|
+
},
|
|
378
|
+
rebind: state.rebind,
|
|
379
|
+
dispose() {
|
|
380
|
+
if (disposed) return
|
|
381
|
+
disposed = true
|
|
382
|
+
host.cancelAnimationFrame(frame)
|
|
383
|
+
reading = null
|
|
384
|
+
state.upAll()
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Every pad connected to the browser, each one on its own: what
|
|
391
|
+
* `Controls.players(...).pool(...)` hands to the player who joins with it.
|
|
392
|
+
* Use {@link actions} for one player, which follows whichever pad is used.
|
|
393
|
+
*/
|
|
394
|
+
export interface Pads<B extends Bindings> extends System.Consumes<"snapshot"> {
|
|
395
|
+
readonly _tag: "Pads"
|
|
396
|
+
/** The most pads read at once: their places in the list run from 0 to `max - 1`. */
|
|
397
|
+
readonly max: number
|
|
398
|
+
/**
|
|
399
|
+
* One snapshot per pad seen so far, in the order they appeared, and
|
|
400
|
+
* starts a new press/release window on each. An unplugged pad stays in
|
|
401
|
+
* the list with `connected: false` and its last `id`, so a player keeps
|
|
402
|
+
* it until it comes back.
|
|
403
|
+
*/
|
|
404
|
+
snapshot(): ReadonlyArray<Snapshot<B>>
|
|
405
|
+
/** Binds the same actions to new buttons on every pad, as {@link Actions.rebind}. */
|
|
406
|
+
rebind<const R extends Rebinding<B>>(bindings: R & OnlyActionsOf<R, B>): void
|
|
407
|
+
/** Stops reading pads and cancels the animation frame. */
|
|
408
|
+
dispose(): void
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Presses and releases `state`'s buttons as `pad` reads now. */
|
|
412
|
+
const readButtons = (state: ReturnType<typeof tracker>, pad: PadReading, triggerThreshold: number): void => {
|
|
413
|
+
for (let index = 0; index < buttonNames.length; index++) {
|
|
414
|
+
const button = pad.buttons[index]
|
|
415
|
+
const name = buttonNames[index]!
|
|
416
|
+
if (button !== undefined && isDown(button, index, triggerThreshold)) state.down(name)
|
|
417
|
+
else state.up(name)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A pad's sticks (dead zone removed) and triggers. */
|
|
422
|
+
const analogOf = (pad: PadReading, deadZone: number) => {
|
|
423
|
+
const axis = (index: number) => pad.axes[index] ?? 0
|
|
424
|
+
const trigger = (index: number) => Math.min(1, Math.max(0, pad.buttons[index]?.value ?? 0))
|
|
425
|
+
return {
|
|
426
|
+
left: stick(axis(0), axis(1), deadZone),
|
|
427
|
+
right: stick(axis(2), axis(3), deadZone),
|
|
428
|
+
leftTrigger: trigger(6),
|
|
429
|
+
rightTrigger: trigger(7)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const resting = { left: zero, right: zero, leftTrigger: 0, rightTrigger: 0 }
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Starts reading every pad on `host`, usually `window`, up to `max` of
|
|
437
|
+
* them, each with its own presses and sticks; pads beyond `max` are
|
|
438
|
+
* ignored. A pad keeps its place in the list while unplugged; one that comes
|
|
439
|
+
* back at the same browser index continues it, and a different pad at that
|
|
440
|
+
* index takes the place over with nothing held.
|
|
441
|
+
*/
|
|
442
|
+
export const pads = <const B extends Bindings>(host: GamepadHost, bindings: B, max: number, options: ActionsOptions = {}): Pads<B> => {
|
|
443
|
+
const deadZone = options.deadZone ?? 0.15
|
|
444
|
+
const triggerThreshold = options.triggerThreshold ?? 0.5
|
|
445
|
+
const seen: Array<{ readonly index: number; readonly state: ReturnType<typeof tracker<B>>; id: string | null; reading: PadReading | null }> = []
|
|
446
|
+
let disposed = false
|
|
447
|
+
let frame = 0
|
|
448
|
+
|
|
449
|
+
const poll = (): void => {
|
|
450
|
+
if (disposed) return
|
|
451
|
+
const connected = readPads(host).filter((pad): pad is PadReading => pad !== null && pad.connected)
|
|
452
|
+
for (const pad of connected) {
|
|
453
|
+
if (seen.length < max && !seen.some((entry) => entry.index === pad.index)) {
|
|
454
|
+
seen.push({ index: pad.index, state: tracker(bindings), id: pad.id, reading: null })
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const entry of seen) {
|
|
458
|
+
const pad = connected.find((reading) => reading.index === entry.index)
|
|
459
|
+
// Unplugged, or another pad took the index: everything it held is released.
|
|
460
|
+
if (pad === undefined || pad.id !== entry.id) {
|
|
461
|
+
if (entry.reading !== null) entry.state.upAll()
|
|
462
|
+
entry.reading = null
|
|
463
|
+
if (pad === undefined) continue
|
|
464
|
+
entry.id = pad.id
|
|
465
|
+
}
|
|
466
|
+
entry.reading = pad
|
|
467
|
+
readButtons(entry.state, pad, triggerThreshold)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const onFrame = (): void => {
|
|
472
|
+
if (disposed) return
|
|
473
|
+
poll()
|
|
474
|
+
frame = host.requestAnimationFrame(onFrame)
|
|
475
|
+
}
|
|
476
|
+
frame = host.requestAnimationFrame(onFrame)
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
_tag: "Pads",
|
|
480
|
+
max,
|
|
481
|
+
snapshot() {
|
|
482
|
+
poll()
|
|
483
|
+
return seen.map((entry) => entry.state.snapshot(entry.reading === null
|
|
484
|
+
? { ...resting, connected: false, id: entry.id }
|
|
485
|
+
: { ...analogOf(entry.reading, deadZone), connected: true, id: entry.id }))
|
|
486
|
+
},
|
|
487
|
+
rebind(next) {
|
|
488
|
+
for (const entry of seen) entry.state.rebind(next as Rebinding<B>)
|
|
489
|
+
// Pads that appear later start with the new bindings too.
|
|
490
|
+
bindings = next as unknown as B
|
|
491
|
+
},
|
|
492
|
+
dispose() {
|
|
493
|
+
if (disposed) return
|
|
494
|
+
disposed = true
|
|
495
|
+
host.cancelAnimationFrame(frame)
|
|
496
|
+
for (const entry of seen) entry.state.upAll()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Pads driven by timelines instead of a host, one timeline per pad (as
|
|
503
|
+
* {@link scripted}), each with the id `scripted <n>` (from `scripted 1`)
|
|
504
|
+
* and `max` the number of timelines: for tests of games where every player
|
|
505
|
+
* has a pad.
|
|
506
|
+
*/
|
|
507
|
+
export const scriptedPads = <const B extends Bindings>(bindings: B, timelines: ReadonlyArray<Timeline>): Pads<B> => {
|
|
508
|
+
const each = timelines.map((timeline) => scripted(bindings, timeline))
|
|
509
|
+
return {
|
|
510
|
+
_tag: "Pads",
|
|
511
|
+
max: each.length,
|
|
512
|
+
snapshot: () => each.map((pad, index) => ({ ...pad.snapshot(), id: `scripted ${index + 1}` })),
|
|
513
|
+
rebind(next) {
|
|
514
|
+
for (const pad of each) pad.rebind(next as never)
|
|
515
|
+
},
|
|
516
|
+
dispose() {}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Input changes applied just before one snapshot: the buttons, then the
|
|
522
|
+
* sticks and triggers, which keep their value until set again, then
|
|
523
|
+
* `connected`. `frame` is the snapshot's index, counting from 0 at the first
|
|
524
|
+
* `snapshot()` call. A button in both `press` and `release` changed twice
|
|
525
|
+
* within the frame: a tap shorter than a frame when it was up, a release and
|
|
526
|
+
* a new press when it was held.
|
|
527
|
+
*/
|
|
528
|
+
export interface TimelineEntry {
|
|
529
|
+
readonly frame: number
|
|
530
|
+
/**
|
|
531
|
+
* Connects or disconnects the pad from this frame on. The first entry
|
|
532
|
+
* connects it unless it says otherwise. A disconnect releases every held
|
|
533
|
+
* button and zeroes the sticks and triggers, as with a live pad.
|
|
534
|
+
*/
|
|
535
|
+
readonly connected?: boolean
|
|
536
|
+
readonly press?: ReadonlyArray<Button>
|
|
537
|
+
readonly release?: ReadonlyArray<Button>
|
|
538
|
+
readonly left?: Vector
|
|
539
|
+
readonly right?: Vector
|
|
540
|
+
readonly leftTrigger?: number
|
|
541
|
+
readonly rightTrigger?: number
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Input changes by snapshot index. Plain data, so it round-trips through
|
|
546
|
+
* JSON; load it back with `parseTimeline`. It names buttons, not actions, so
|
|
547
|
+
* a replay follows the bindings as live play does.
|
|
548
|
+
*/
|
|
549
|
+
export type Timeline = ReadonlyArray<TimelineEntry>
|
|
550
|
+
|
|
551
|
+
/** Actions driven by a timeline instead of a host. */
|
|
552
|
+
export interface Scripted<B extends Bindings> extends Actions<B> {
|
|
553
|
+
/** Number of snapshots taken so far, which is the next frame index. */
|
|
554
|
+
readonly frames: () => number
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Plays `timeline`: before the snapshot at index `frame`, every entry for
|
|
559
|
+
* that frame is applied. A pad counts as connected (with the id
|
|
560
|
+
* `"scripted"`) from the first entry on, until an entry says
|
|
561
|
+
* `connected: false`; `scripted(bindings, [])` is a pad that never connects.
|
|
562
|
+
* Buttons stay held from their press until their release; pressing a held
|
|
563
|
+
* button is not a new press.
|
|
564
|
+
*/
|
|
565
|
+
export const scripted = <const B extends Bindings>(bindings: B, timeline: Timeline): Scripted<B> => {
|
|
566
|
+
const state = tracker(bindings)
|
|
567
|
+
const byFrame = new Map<number, Array<TimelineEntry>>()
|
|
568
|
+
for (const entry of timeline) {
|
|
569
|
+
const entries = byFrame.get(entry.frame)
|
|
570
|
+
if (entries) entries.push(entry)
|
|
571
|
+
else byFrame.set(entry.frame, [entry])
|
|
572
|
+
}
|
|
573
|
+
const first = timeline.reduce((earliest, entry) => Math.min(earliest, entry.frame), Number.POSITIVE_INFINITY)
|
|
574
|
+
const resting = { left: zero, right: zero, leftTrigger: 0, rightTrigger: 0 }
|
|
575
|
+
let analog = resting
|
|
576
|
+
let connected = false
|
|
577
|
+
let frame = 0
|
|
578
|
+
return {
|
|
579
|
+
snapshot() {
|
|
580
|
+
for (const entry of byFrame.get(frame) ?? []) {
|
|
581
|
+
const releases = new Set(entry.release ?? [])
|
|
582
|
+
for (const button of entry.press ?? []) {
|
|
583
|
+
if (state.held.has(button) && releases.has(button)) {
|
|
584
|
+
// Released and pressed again within the frame.
|
|
585
|
+
releases.delete(button)
|
|
586
|
+
state.up(button)
|
|
587
|
+
}
|
|
588
|
+
state.down(button)
|
|
589
|
+
}
|
|
590
|
+
for (const button of releases) state.up(button)
|
|
591
|
+
analog = {
|
|
592
|
+
left: entry.left ?? analog.left,
|
|
593
|
+
right: entry.right ?? analog.right,
|
|
594
|
+
leftTrigger: entry.leftTrigger ?? analog.leftTrigger,
|
|
595
|
+
rightTrigger: entry.rightTrigger ?? analog.rightTrigger
|
|
596
|
+
}
|
|
597
|
+
connected = entry.connected ?? (frame === first ? true : connected)
|
|
598
|
+
if (!connected) {
|
|
599
|
+
state.upAll()
|
|
600
|
+
analog = resting
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
frame += 1
|
|
604
|
+
return state.snapshot({ ...analog, connected, id: connected ? "scripted" : null })
|
|
605
|
+
},
|
|
606
|
+
rebind: state.rebind,
|
|
607
|
+
dispose() {},
|
|
608
|
+
frames: () => frame
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Live actions that also record the timeline they produce. */
|
|
613
|
+
export interface Recording<B extends Bindings> extends Actions<B> {
|
|
614
|
+
/** The recorded timeline; `scripted(bindings, timeline)` replays it exactly. */
|
|
615
|
+
readonly timeline: () => Timeline
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Wraps `actions` and records, for every snapshot, the buttons pressed and
|
|
620
|
+
* released since the previous one and the sticks and triggers that moved.
|
|
621
|
+
*/
|
|
622
|
+
export const recording = <B extends Bindings>(actions: Actions<B>): Recording<B> => {
|
|
623
|
+
const entries: Array<TimelineEntry> = []
|
|
624
|
+
let previous: Omit<Snapshot<B>, "actions" | "vectors" | "id" | "used"> = { buttons: idleButtons(buttonNames), left: zero, right: zero, leftTrigger: 0, rightTrigger: 0, connected: false }
|
|
625
|
+
let frame = 0
|
|
626
|
+
return {
|
|
627
|
+
snapshot() {
|
|
628
|
+
const snapshot = actions.snapshot()
|
|
629
|
+
const press = buttonNames.filter((name) => snapshot.buttons[name].pressed)
|
|
630
|
+
// A press of a button held at the previous snapshot means it was released in between.
|
|
631
|
+
const release = buttonNames.filter((name) => snapshot.buttons[name].released || (snapshot.buttons[name].pressed && previous.buttons[name].held))
|
|
632
|
+
const entry: Record<string, unknown> = {
|
|
633
|
+
...(press.length > 0 ? { press } : {}),
|
|
634
|
+
...(release.length > 0 ? { release } : {}),
|
|
635
|
+
...(sameVector(snapshot.left, previous.left) ? {} : { left: snapshot.left }),
|
|
636
|
+
...(sameVector(snapshot.right, previous.right) ? {} : { right: snapshot.right }),
|
|
637
|
+
...(snapshot.leftTrigger === previous.leftTrigger ? {} : { leftTrigger: snapshot.leftTrigger }),
|
|
638
|
+
...(snapshot.rightTrigger === previous.rightTrigger ? {} : { rightTrigger: snapshot.rightTrigger }),
|
|
639
|
+
// The first entry connects the pad by itself; later changes are written out.
|
|
640
|
+
...(snapshot.connected === previous.connected || entries.length === 0 ? {} : { connected: snapshot.connected })
|
|
641
|
+
}
|
|
642
|
+
// The first entry also marks when a pad appeared, so a replay connects on the same frame.
|
|
643
|
+
if (Object.keys(entry).length > 0 || (entries.length === 0 && snapshot.connected)) entries.push({ frame, ...entry })
|
|
644
|
+
previous = snapshot
|
|
645
|
+
frame += 1
|
|
646
|
+
return snapshot
|
|
647
|
+
},
|
|
648
|
+
rebind: (bindings) => actions.rebind(bindings),
|
|
649
|
+
dispose: () => actions.dispose(),
|
|
650
|
+
timeline: () => [...entries]
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Why loaded data is not a timeline. `path` points at the offending value,
|
|
656
|
+
* for example `[3].press[0]`.
|
|
657
|
+
*/
|
|
658
|
+
export interface InvalidTimeline {
|
|
659
|
+
readonly _tag: "InvalidTimeline"
|
|
660
|
+
readonly path: string
|
|
661
|
+
readonly message: string
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const buttonSet: ReadonlySet<string> = new Set(buttonNames)
|
|
665
|
+
|
|
666
|
+
/** Whether `name` is a button of the standard mapping, for validating stored bindings. */
|
|
667
|
+
export const isButton = (name: unknown): name is Button => typeof name === "string" && buttonSet.has(name)
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Validates untrusted data, such as a parsed JSON file, as a timeline: an
|
|
671
|
+
* array of entries with a non-negative integer `frame`, optional
|
|
672
|
+
* `press`/`release` arrays of button names, optional `left`/`right` vectors
|
|
673
|
+
* of length at most 1, optional triggers from 0 to 1, and an optional
|
|
674
|
+
* boolean `connected`.
|
|
675
|
+
*/
|
|
676
|
+
export const parseTimeline = (data: unknown): Result.Result<Timeline, InvalidTimeline> => {
|
|
677
|
+
const invalid = (path: string, message: string) => Result.failure<InvalidTimeline>({ _tag: "InvalidTimeline", path, message })
|
|
678
|
+
if (!Array.isArray(data)) return invalid("", "expected an array of entries")
|
|
679
|
+
const entries: Array<TimelineEntry> = []
|
|
680
|
+
for (let index = 0; index < data.length; index++) {
|
|
681
|
+
const entry: unknown = data[index]
|
|
682
|
+
const path = `[${index}]`
|
|
683
|
+
if (typeof entry !== "object" || entry === null) return invalid(path, "expected an object")
|
|
684
|
+
const fields = entry as Readonly<Record<string, unknown>>
|
|
685
|
+
const frame = fields["frame"]
|
|
686
|
+
if (typeof frame !== "number" || !Number.isInteger(frame) || frame < 0) return invalid(`${path}.frame`, "expected a non-negative integer")
|
|
687
|
+
const parsed: Record<string, unknown> = { frame }
|
|
688
|
+
for (const field of ["press", "release"] as const) {
|
|
689
|
+
const value = fields[field]
|
|
690
|
+
if (value === undefined) continue
|
|
691
|
+
if (!Array.isArray(value)) return invalid(`${path}.${field}`, "expected an array of button names")
|
|
692
|
+
for (let offset = 0; offset < value.length; offset++) {
|
|
693
|
+
if (!isButton(value[offset])) return invalid(`${path}.${field}[${offset}]`, `expected one of ${buttonNames.join(", ")}`)
|
|
694
|
+
}
|
|
695
|
+
parsed[field] = [...value]
|
|
696
|
+
}
|
|
697
|
+
for (const field of ["left", "right"] as const) {
|
|
698
|
+
const value = fields[field]
|
|
699
|
+
if (value === undefined) continue
|
|
700
|
+
const { x, y } = (typeof value === "object" && value !== null ? value : {}) as { readonly x?: unknown; readonly y?: unknown }
|
|
701
|
+
if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || x * x + y * y > 1 + 1e-9) {
|
|
702
|
+
return invalid(`${path}.${field}`, "expected { x, y } of length at most 1")
|
|
703
|
+
}
|
|
704
|
+
parsed[field] = { x, y }
|
|
705
|
+
}
|
|
706
|
+
for (const field of ["leftTrigger", "rightTrigger"] as const) {
|
|
707
|
+
const value = fields[field]
|
|
708
|
+
if (value === undefined) continue
|
|
709
|
+
if (typeof value !== "number" || !(value >= 0 && value <= 1)) return invalid(`${path}.${field}`, "expected a number from 0 to 1")
|
|
710
|
+
parsed[field] = value
|
|
711
|
+
}
|
|
712
|
+
const connected = fields["connected"]
|
|
713
|
+
if (connected !== undefined) {
|
|
714
|
+
if (typeof connected !== "boolean") return invalid(`${path}.connected`, "expected a boolean")
|
|
715
|
+
parsed["connected"] = connected
|
|
716
|
+
}
|
|
717
|
+
entries.push(parsed as unknown as TimelineEntry)
|
|
718
|
+
}
|
|
719
|
+
return Result.success(entries)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Why loaded data is not a rebinding for the bindings. `path` points at the
|
|
724
|
+
* offending value, for example `attack[1]`.
|
|
725
|
+
*/
|
|
726
|
+
export type InvalidRebinding = ButtonsInternal.InvalidRebinding
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Validates untrusted data, such as saved settings parsed from JSON, as new
|
|
730
|
+
* buttons for `bindings`: an object with exactly the same action names, each
|
|
731
|
+
* mapped to a non-empty array of button names (for a vector action, an
|
|
732
|
+
* object with an optional `stick` and buttons for all four directions or
|
|
733
|
+
* none, as `Gamepad.vector` saves). Pass the result to
|
|
734
|
+
* {@link Actions.rebind}; on failure, keep the defaults.
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```ts
|
|
738
|
+
* const saved = Gamepad.parseRebinding(bindings, JSON.parse(stored))
|
|
739
|
+
* if (saved.ok) pad.rebind(saved.value)
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
export const parseRebinding = <const B extends Bindings>(
|
|
743
|
+
bindings: B,
|
|
744
|
+
data: unknown
|
|
745
|
+
): Result.Result<Rebinding<B>, InvalidRebinding> =>
|
|
746
|
+
// The checked names and buttons are exactly `Rebinding<B>`.
|
|
747
|
+
ButtonsInternal.parseButtonRebinding(buttonNames, bindings, data, ["left", "right"]) as Result.Result<Rebinding<B>, InvalidRebinding>
|