dualsense-ts 2.2.48 → 3.1.3
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 +9 -2
- package/dist/comparators.d.ts +13 -0
- package/dist/comparators.d.ts.map +1 -0
- package/dist/comparators.js +27 -0
- package/dist/comparators.js.map +1 -0
- package/dist/dualsense.d.ts +10 -1
- package/dist/dualsense.d.ts.map +1 -1
- package/dist/dualsense.js +46 -45
- package/dist/dualsense.js.map +1 -1
- package/dist/elements/analog.d.ts +12 -7
- package/dist/elements/analog.d.ts.map +1 -1
- package/dist/elements/analog.js +29 -11
- package/dist/elements/analog.js.map +1 -1
- package/dist/elements/axis.d.ts +22 -1
- package/dist/elements/axis.d.ts.map +1 -1
- package/dist/elements/axis.js +24 -4
- package/dist/elements/axis.js.map +1 -1
- package/dist/elements/dpad.d.ts +1 -1
- package/dist/elements/dpad.d.ts.map +1 -1
- package/dist/elements/dpad.js +6 -5
- package/dist/elements/dpad.js.map +1 -1
- package/dist/elements/touch.d.ts +2 -1
- package/dist/elements/touch.d.ts.map +1 -1
- package/dist/elements/touch.js +1 -0
- package/dist/elements/touch.js.map +1 -1
- package/dist/elements/touchpad.d.ts +1 -1
- package/dist/elements/touchpad.d.ts.map +1 -1
- package/dist/elements/touchpad.js.map +1 -1
- package/dist/elements/trigger.d.ts.map +1 -1
- package/dist/elements/trigger.js +1 -1
- package/dist/elements/trigger.js.map +1 -1
- package/dist/elements/unisense.d.ts +3 -3
- package/dist/elements/unisense.d.ts.map +1 -1
- package/dist/elements/unisense.js +13 -5
- package/dist/elements/unisense.js.map +1 -1
- package/dist/hid/dualsense_hid.d.ts +16 -58
- package/dist/hid/dualsense_hid.d.ts.map +1 -1
- package/dist/hid/dualsense_hid.js +25 -101
- package/dist/hid/dualsense_hid.js.map +1 -1
- package/dist/hid/hid_provider.d.ts +86 -0
- package/dist/hid/hid_provider.d.ts.map +1 -0
- package/dist/hid/hid_provider.js +45 -0
- package/dist/hid/hid_provider.js.map +1 -0
- package/dist/hid/index.d.ts +4 -1
- package/dist/hid/index.d.ts.map +1 -1
- package/dist/hid/index.js +4 -1
- package/dist/hid/index.js.map +1 -1
- package/dist/hid/node_hid_provider.d.ts +11 -0
- package/dist/hid/node_hid_provider.d.ts.map +1 -0
- package/dist/hid/node_hid_provider.js +97 -0
- package/dist/hid/node_hid_provider.js.map +1 -0
- package/dist/hid/platform_hid_provider.d.ts +4 -0
- package/dist/hid/platform_hid_provider.d.ts.map +1 -0
- package/dist/hid/platform_hid_provider.js +7 -0
- package/dist/hid/platform_hid_provider.js.map +1 -0
- package/dist/hid/web_hid_provider.d.ts +10 -0
- package/dist/hid/web_hid_provider.d.ts.map +1 -0
- package/dist/hid/web_hid_provider.js +102 -0
- package/dist/hid/web_hid_provider.js.map +1 -0
- package/dist/{hid/ids.d.ts → id.d.ts} +6 -2
- package/dist/id.d.ts.map +1 -0
- package/dist/{hid/ids.js → id.js} +1 -1
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +63 -45
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +123 -90
- package/dist/input.js.map +1 -1
- package/package.json +14 -8
- package/src/comparators.ts +26 -0
- package/src/dualsense.ts +61 -58
- package/src/elements/analog.spec.ts +2 -15
- package/src/elements/analog.ts +41 -22
- package/src/elements/axis.ts +35 -3
- package/src/elements/dpad.ts +7 -6
- package/src/elements/touch.ts +2 -1
- package/src/elements/touchpad.ts +1 -1
- package/src/elements/trigger.ts +1 -1
- package/src/elements/unisense.ts +19 -17
- package/src/hid/dualsense_hid.ts +25 -156
- package/src/hid/{dualsense_hid.spec.ts → hid_provider.spec.ts} +1 -1
- package/src/hid/hid_provider.ts +100 -0
- package/src/hid/index.ts +4 -1
- package/src/hid/node_hid_provider.ts +108 -0
- package/src/hid/platform_hid_provider.ts +4 -0
- package/src/hid/web_hid_provider.ts +116 -0
- package/src/{hid/ids.ts → id.ts} +6 -1
- package/src/index.ts +2 -0
- package/src/input.ts +156 -138
- package/src/readme.spec.ts +6 -8
- package/webpack.config.js +42 -0
- package/dist/hid/ids.d.ts.map +0 -1
- package/dist/hid/ids.js.map +0 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { HID, devices } from "node-hid";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
HIDProvider,
|
|
5
|
+
DualsenseHIDState,
|
|
6
|
+
InputId,
|
|
7
|
+
mapAxis,
|
|
8
|
+
mapTrigger,
|
|
9
|
+
} from "./hid_provider";
|
|
10
|
+
|
|
11
|
+
export class NodeHIDProvider extends HIDProvider {
|
|
12
|
+
private device?: HID;
|
|
13
|
+
public wireless: boolean = false;
|
|
14
|
+
|
|
15
|
+
connect(): void {
|
|
16
|
+
this.disconnect();
|
|
17
|
+
|
|
18
|
+
const controllers = devices(HIDProvider.vendorId, HIDProvider.productId);
|
|
19
|
+
if (controllers.length === 0 || !controllers[0].path) {
|
|
20
|
+
return this.onError(
|
|
21
|
+
new Error(`No controllers (${devices().length} other devices)`)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (controllers[0].interface === -1) this.wireless = true;
|
|
26
|
+
|
|
27
|
+
this.device = new HID(controllers[0].path);
|
|
28
|
+
this.device.on("data", (arg: Buffer) => {
|
|
29
|
+
this.onData(this.process(arg));
|
|
30
|
+
});
|
|
31
|
+
this.device.on("error", (err: Error) => {
|
|
32
|
+
this.onError(err);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get connected(): boolean {
|
|
37
|
+
return this.device !== undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnect(): void {
|
|
41
|
+
if (this.device) {
|
|
42
|
+
this.device.removeAllListeners();
|
|
43
|
+
this.device.close();
|
|
44
|
+
this.device = undefined;
|
|
45
|
+
this.wireless = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process(buffer: Buffer): DualsenseHIDState {
|
|
50
|
+
// Bluetooth buffer starts with an extra byte
|
|
51
|
+
const report = buffer.subarray(this.wireless ? 2 : 1);
|
|
52
|
+
|
|
53
|
+
const mainButtons = report.readUint8(7);
|
|
54
|
+
const miscButtons = report.readUint8(8);
|
|
55
|
+
const lastButtons = report.readUint8(9);
|
|
56
|
+
const dpad = (mainButtons << 4) >> 4;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
[InputId.LeftAnalogX]: mapAxis(report.readUint8(0)),
|
|
60
|
+
[InputId.LeftAnalogY]: -mapAxis(report.readUint8(1)),
|
|
61
|
+
[InputId.RightAnalogX]: mapAxis(report.readUint8(2)),
|
|
62
|
+
[InputId.RightAnalogY]: -mapAxis(report.readUint8(3)),
|
|
63
|
+
[InputId.LeftTrigger]: mapTrigger(report.readUint8(4)),
|
|
64
|
+
[InputId.RightTrigger]: mapTrigger(report.readUint8(5)),
|
|
65
|
+
// 6 is a sequence byte
|
|
66
|
+
[InputId.Triangle]: (mainButtons & 128) > 0,
|
|
67
|
+
[InputId.Circle]: (mainButtons & 64) > 0,
|
|
68
|
+
[InputId.Cross]: (mainButtons & 32) > 0,
|
|
69
|
+
[InputId.Square]: (mainButtons & 16) > 0,
|
|
70
|
+
[InputId.Dpad]: dpad,
|
|
71
|
+
[InputId.Up]: dpad < 2 || dpad === 7,
|
|
72
|
+
[InputId.Down]: dpad > 2 && dpad < 6,
|
|
73
|
+
[InputId.Left]: dpad > 4 && dpad < 8,
|
|
74
|
+
[InputId.Right]: dpad > 0 && dpad < 4,
|
|
75
|
+
[InputId.LeftTriggerButton]: (miscButtons & 4) > 0,
|
|
76
|
+
[InputId.RightTriggerButton]: (miscButtons & 8) > 0,
|
|
77
|
+
[InputId.LeftBumper]: (miscButtons & 1) > 0,
|
|
78
|
+
[InputId.RightBumper]: (miscButtons & 2) > 0,
|
|
79
|
+
[InputId.Create]: (miscButtons & 16) > 0,
|
|
80
|
+
[InputId.Options]: (miscButtons & 32) > 0,
|
|
81
|
+
[InputId.LeftAnalogButton]: (miscButtons & 64) > 0,
|
|
82
|
+
[InputId.RightAnalogButton]: (miscButtons & 128) > 0,
|
|
83
|
+
[InputId.Playstation]: (lastButtons & 1) > 0,
|
|
84
|
+
[InputId.TouchButton]: (lastButtons & 2) > 0,
|
|
85
|
+
[InputId.Mute]: (lastButtons & 4) > 0,
|
|
86
|
+
// The other 5 bits are unused
|
|
87
|
+
// 5 reserved bytes
|
|
88
|
+
[InputId.GyroX]: report.readUint16LE(15),
|
|
89
|
+
[InputId.GyroY]: report.readUint16LE(17),
|
|
90
|
+
[InputId.GyroZ]: report.readUint16LE(19),
|
|
91
|
+
[InputId.AccelX]: report.readUint16LE(21),
|
|
92
|
+
[InputId.AccelY]: report.readUint16LE(23),
|
|
93
|
+
[InputId.AccelZ]: report.readUint16LE(25),
|
|
94
|
+
// 4 bytes for sensor timestamp (32LE)
|
|
95
|
+
// 1 reserved byte
|
|
96
|
+
[InputId.TouchId0]: report.readUint8(32) & 0x7f,
|
|
97
|
+
[InputId.TouchContact0]: (report.readUint8(32) & 0x80) === 0,
|
|
98
|
+
[InputId.TouchX0]: mapAxis((report.readUint16LE(33) << 20) >> 20, 1920),
|
|
99
|
+
[InputId.TouchY0]: mapAxis(report.readUint16LE(34) >> 4, 1080),
|
|
100
|
+
[InputId.TouchId1]: report.readUint8(36) & 0x7f,
|
|
101
|
+
[InputId.TouchContact1]: (report.readUint8(36) & 0x80) === 0,
|
|
102
|
+
[InputId.TouchX1]: mapAxis((report.readUint16LE(37) << 20) >> 20, 1920),
|
|
103
|
+
[InputId.TouchY1]: mapAxis(report.readUint16LE(38) >> 4, 1080),
|
|
104
|
+
// 12 reserved bytes
|
|
105
|
+
[InputId.Status]: (report.readUint8(53) & 4) > 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HIDProvider,
|
|
3
|
+
DualsenseHIDState,
|
|
4
|
+
InputId,
|
|
5
|
+
mapAxis,
|
|
6
|
+
mapTrigger,
|
|
7
|
+
} from "./hid_provider";
|
|
8
|
+
|
|
9
|
+
export class WebHIDProvider extends HIDProvider {
|
|
10
|
+
private device?: HIDDevice;
|
|
11
|
+
public wireless: boolean = true; // TODO: Not sure what to check
|
|
12
|
+
|
|
13
|
+
connect(): void {
|
|
14
|
+
this.disconnect();
|
|
15
|
+
|
|
16
|
+
navigator.hid
|
|
17
|
+
.requestDevice({
|
|
18
|
+
filters: [
|
|
19
|
+
{ vendorId: HIDProvider.vendorId, productId: HIDProvider.productId },
|
|
20
|
+
],
|
|
21
|
+
})
|
|
22
|
+
.then((devices: HIDDevice[]) => {
|
|
23
|
+
if (devices.length === 0) {
|
|
24
|
+
return this.onError(new Error(`No controllers available`));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
devices[0]
|
|
28
|
+
.open()
|
|
29
|
+
.then(() => {
|
|
30
|
+
this.device = devices[0];
|
|
31
|
+
this.device.addEventListener("inputreport", ({ data }) => {
|
|
32
|
+
this.onData(this.process(data));
|
|
33
|
+
});
|
|
34
|
+
})
|
|
35
|
+
.catch((err: Error) => {
|
|
36
|
+
this.onError(err);
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
.catch((err: Error) => {
|
|
40
|
+
this.onError(err);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get connected(): boolean {
|
|
45
|
+
return this.device !== undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
disconnect(): void {
|
|
49
|
+
if (this.device) {
|
|
50
|
+
this.device.close().finally(() => {
|
|
51
|
+
this.device = undefined;
|
|
52
|
+
this.wireless = false;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process(buffer: DataView): DualsenseHIDState {
|
|
58
|
+
// Bluetooth buffer starts with an extra byte
|
|
59
|
+
const report = new DataView(buffer.buffer, this.wireless ? 2 : 1);
|
|
60
|
+
|
|
61
|
+
const mainButtons = report.getUint8(7);
|
|
62
|
+
const miscButtons = report.getUint8(8);
|
|
63
|
+
const lastButtons = report.getUint8(9);
|
|
64
|
+
const dpad = (mainButtons << 4) >> 4;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
[InputId.LeftAnalogX]: mapAxis(report.getUint8(0)),
|
|
68
|
+
[InputId.LeftAnalogY]: -mapAxis(report.getUint8(1)),
|
|
69
|
+
[InputId.RightAnalogX]: mapAxis(report.getUint8(2)),
|
|
70
|
+
[InputId.RightAnalogY]: -mapAxis(report.getUint8(3)),
|
|
71
|
+
[InputId.LeftTrigger]: mapTrigger(report.getUint8(4)),
|
|
72
|
+
[InputId.RightTrigger]: mapTrigger(report.getUint8(5)),
|
|
73
|
+
[InputId.Triangle]: (mainButtons & 128) > 0,
|
|
74
|
+
[InputId.Circle]: (mainButtons & 64) > 0,
|
|
75
|
+
[InputId.Cross]: (mainButtons & 32) > 0,
|
|
76
|
+
[InputId.Square]: (mainButtons & 16) > 0,
|
|
77
|
+
[InputId.Dpad]: dpad,
|
|
78
|
+
[InputId.Up]: dpad < 2 || dpad === 7,
|
|
79
|
+
[InputId.Down]: dpad > 2 && dpad < 6,
|
|
80
|
+
[InputId.Left]: dpad > 4 && dpad < 8,
|
|
81
|
+
[InputId.Right]: dpad > 0 && dpad < 4,
|
|
82
|
+
[InputId.LeftTriggerButton]: (miscButtons & 4) > 0,
|
|
83
|
+
[InputId.RightTriggerButton]: (miscButtons & 8) > 0,
|
|
84
|
+
[InputId.LeftBumper]: (miscButtons & 1) > 0,
|
|
85
|
+
[InputId.RightBumper]: (miscButtons & 2) > 0,
|
|
86
|
+
[InputId.Create]: (miscButtons & 16) > 0,
|
|
87
|
+
[InputId.Options]: (miscButtons & 32) > 0,
|
|
88
|
+
[InputId.LeftAnalogButton]: (miscButtons & 64) > 0,
|
|
89
|
+
[InputId.RightAnalogButton]: (miscButtons & 128) > 0,
|
|
90
|
+
[InputId.Playstation]: (lastButtons & 1) > 0,
|
|
91
|
+
[InputId.TouchButton]: (lastButtons & 2) > 0,
|
|
92
|
+
[InputId.Mute]: (lastButtons & 4) > 0,
|
|
93
|
+
[InputId.GyroX]: report.getUint16(15, true),
|
|
94
|
+
[InputId.GyroY]: report.getUint16(17, true),
|
|
95
|
+
[InputId.GyroZ]: report.getUint16(19, true),
|
|
96
|
+
[InputId.AccelX]: report.getUint16(21, true),
|
|
97
|
+
[InputId.AccelY]: report.getUint16(23, true),
|
|
98
|
+
[InputId.AccelZ]: report.getUint16(25, true),
|
|
99
|
+
[InputId.TouchId0]: report.getUint8(32) & 0x7f,
|
|
100
|
+
[InputId.TouchContact0]: (report.getUint8(32) & 0x80) === 0,
|
|
101
|
+
[InputId.TouchX0]: mapAxis(
|
|
102
|
+
(report.getUint16(33, true) << 20) >> 20,
|
|
103
|
+
1920
|
|
104
|
+
),
|
|
105
|
+
[InputId.TouchY0]: mapAxis(report.getUint16(34, true) >> 4, 1080),
|
|
106
|
+
[InputId.TouchId1]: report.getUint8(36) & 0x7f,
|
|
107
|
+
[InputId.TouchContact1]: (report.getUint8(36) & 0x80) === 0,
|
|
108
|
+
[InputId.TouchX1]: mapAxis(
|
|
109
|
+
(report.getUint16(37, true) << 20) >> 20,
|
|
110
|
+
1920
|
|
111
|
+
),
|
|
112
|
+
[InputId.TouchY1]: mapAxis(report.getUint16(38, true) >> 4, 1080),
|
|
113
|
+
[InputId.Status]: (report.getUint8(53) & 4) > 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/{hid/ids.ts → id.ts}
RENAMED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* IDs for real and virtual controller inputs.
|
|
3
|
+
*/
|
|
2
4
|
export const enum InputId {
|
|
3
5
|
Options = "Options",
|
|
4
6
|
Create = "Create",
|
|
@@ -52,4 +54,7 @@ export const enum InputId {
|
|
|
52
54
|
AccelX = "AccelX",
|
|
53
55
|
AccelY = "AccelY",
|
|
54
56
|
AccelZ = "AccelZ",
|
|
57
|
+
|
|
58
|
+
// For placeholder inputs
|
|
59
|
+
Unknown = "Unknown",
|
|
55
60
|
}
|
package/src/index.ts
CHANGED
package/src/input.ts
CHANGED
|
@@ -1,93 +1,177 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { InputId } from "./id";
|
|
2
|
+
import {
|
|
3
|
+
VirtualComparator,
|
|
4
|
+
ThresholdComparator,
|
|
5
|
+
BasicComparator,
|
|
6
|
+
} from "./comparators";
|
|
7
|
+
|
|
8
|
+
export { InputId } from "./id";
|
|
3
9
|
|
|
4
10
|
export interface InputParams {
|
|
5
11
|
name?: string;
|
|
6
12
|
icon?: string;
|
|
7
13
|
threshold?: number;
|
|
8
|
-
parent?: Input<unknown>;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
icon: "???",
|
|
14
|
-
threshold: 0,
|
|
15
|
-
};
|
|
16
|
+
export type InputChangeType = "change" | "press" | "release";
|
|
17
|
+
export type InputEventType = InputChangeType | "input";
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Optional abstract properties
|
|
27
|
-
export const InputChanged = Symbol("InputChanged");
|
|
19
|
+
export type InputCallback<Instance> = (
|
|
20
|
+
input: Instance,
|
|
21
|
+
changed: Instance | Input<unknown>
|
|
22
|
+
) => unknown | Promise<unknown>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Private utilities
|
|
26
|
+
*/
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
export const InputSetComparator = Symbol("InputSetComparator");
|
|
29
|
+
export const InputChanged = Symbol("InputChanged");
|
|
30
30
|
export const InputSet = Symbol("InputSet");
|
|
31
31
|
export const InputName = Symbol("InputName");
|
|
32
32
|
export const InputIcon = Symbol("InputIcon");
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
): this;
|
|
44
|
-
emit(
|
|
45
|
-
event: InputEvent,
|
|
46
|
-
...args: [Input<Type>, Input<unknown> | Input<Type>]
|
|
47
|
-
): boolean;
|
|
48
|
-
}
|
|
34
|
+
/**
|
|
35
|
+
* Private properties
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const InputOns = Symbol("InputOns");
|
|
39
|
+
const InputOnces = Symbol("InputOnces");
|
|
40
|
+
const InputAdopt = Symbol("InputAdopt");
|
|
41
|
+
const InputParents = Symbol("InputParents");
|
|
42
|
+
const InputComparator = Symbol("InputComparator");
|
|
49
43
|
|
|
50
44
|
/**
|
|
51
45
|
* Input manages the state of a single device input,
|
|
52
46
|
* a virtual input, or a group of Input children.
|
|
53
47
|
*/
|
|
54
|
-
export abstract class Input<Type>
|
|
55
|
-
|
|
56
|
-
implements AsyncIterator<Input<Type>>
|
|
57
|
-
{
|
|
58
|
-
public readonly id: symbol;
|
|
48
|
+
export abstract class Input<Type> implements AsyncIterator<Input<Type>> {
|
|
49
|
+
public readonly id: InputId = InputId.Unknown;
|
|
59
50
|
|
|
60
51
|
/**
|
|
61
|
-
*
|
|
52
|
+
* For numeric inputs, ignore state changes smaller than this threshold.
|
|
62
53
|
*/
|
|
63
|
-
public
|
|
54
|
+
public threshold: number = 0;
|
|
64
55
|
|
|
65
56
|
/**
|
|
66
|
-
*
|
|
57
|
+
* Provide the type and default value for the input.
|
|
67
58
|
*/
|
|
68
|
-
public
|
|
59
|
+
public abstract state: Type;
|
|
69
60
|
|
|
70
61
|
/**
|
|
71
|
-
*
|
|
62
|
+
* Stores event listeners.
|
|
72
63
|
*/
|
|
73
|
-
|
|
64
|
+
private [InputOns] = new Map<InputEventType, InputCallback<this>[]>();
|
|
74
65
|
|
|
75
66
|
/**
|
|
76
|
-
*
|
|
67
|
+
* Stores callbacks waiting for one-time events.
|
|
77
68
|
*/
|
|
78
|
-
|
|
69
|
+
private [InputOnces] = new Map<InputChangeType, InputCallback<this>[]>();
|
|
70
|
+
|
|
71
|
+
constructor(params: InputParams = {}) {
|
|
72
|
+
const { name, icon, threshold } = {
|
|
73
|
+
icon: "???",
|
|
74
|
+
name: "Nameless Input",
|
|
75
|
+
threshold: 0,
|
|
76
|
+
...params,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this[InputName] = name;
|
|
80
|
+
this[InputIcon] = icon;
|
|
81
|
+
this.threshold = threshold;
|
|
82
|
+
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
this[InputSetComparator]();
|
|
85
|
+
Object.values(this).forEach((value) => {
|
|
86
|
+
if (value === this) return;
|
|
87
|
+
if (value instanceof Input) value[InputAdopt](this as Input<unknown>);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
81
93
|
* Implement a function that returns true if the user is actively engaged with the input.
|
|
82
94
|
*/
|
|
83
95
|
public abstract get active(): boolean;
|
|
84
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Register a callback to recieve state updates from this Input.
|
|
99
|
+
*/
|
|
100
|
+
public on(event: InputEventType, listener: InputCallback<this>): this {
|
|
101
|
+
const listeners = this[InputOns].get(event);
|
|
102
|
+
if (!listeners) {
|
|
103
|
+
this[InputOns].set(event, []);
|
|
104
|
+
return this.on(event, listener);
|
|
105
|
+
}
|
|
106
|
+
listeners.push(listener);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a callback to recieve the next specified update.
|
|
112
|
+
*/
|
|
113
|
+
public once(event: InputChangeType, listener: InputCallback<this>): this {
|
|
114
|
+
const listeners = this[InputOnces].get(event);
|
|
115
|
+
if (!listeners) {
|
|
116
|
+
this[InputOnces].set(event, []);
|
|
117
|
+
return this.once(event, listener);
|
|
118
|
+
}
|
|
119
|
+
listeners.push(listener);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Notify listeners and parents of a state change.
|
|
125
|
+
*/
|
|
126
|
+
private emit(event: InputEventType, changed: Input<unknown> | this): void {
|
|
127
|
+
const listeners = this[InputOns].get(event) || [];
|
|
128
|
+
listeners.forEach((callback) => {
|
|
129
|
+
callback(this, changed);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (event !== "input") {
|
|
133
|
+
this.emitOnce(event, changed);
|
|
134
|
+
this[InputParents].forEach((input) => {
|
|
135
|
+
input.emit(event, changed as Input<unknown>);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Notify one-time listeners of a state change.
|
|
142
|
+
*/
|
|
143
|
+
private emitOnce(
|
|
144
|
+
event: InputChangeType,
|
|
145
|
+
changed: this | Input<unknown> = this
|
|
146
|
+
): void {
|
|
147
|
+
const listeners = this[InputOnces].get(event) || [];
|
|
148
|
+
this[InputOnces].set(event, []);
|
|
149
|
+
listeners.forEach((callback) => {
|
|
150
|
+
callback(this, changed as Input<unknown>);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Register a callback to recieve state updates from this Input.
|
|
156
|
+
*/
|
|
157
|
+
public addEventListener(
|
|
158
|
+
event: InputEventType,
|
|
159
|
+
listener: InputCallback<this>,
|
|
160
|
+
{ once }: { once: boolean } = { once: false }
|
|
161
|
+
): this {
|
|
162
|
+
if (once) {
|
|
163
|
+
if (event === "input") {
|
|
164
|
+
throw new Error("Can't listen once to `input` events");
|
|
165
|
+
}
|
|
166
|
+
return this.once(event, listener);
|
|
167
|
+
}
|
|
168
|
+
return this.on(event, listener);
|
|
169
|
+
}
|
|
170
|
+
|
|
85
171
|
/**
|
|
86
172
|
* Resolves on the next change to this input's state.
|
|
87
173
|
*/
|
|
88
|
-
public next(
|
|
89
|
-
type: "press" | "release" | "change" = "change"
|
|
90
|
-
): Promise<IteratorResult<this>> {
|
|
174
|
+
public next(type: InputChangeType = "change"): Promise<IteratorResult<this>> {
|
|
91
175
|
return new Promise<IteratorResult<this>>((resolve) => {
|
|
92
176
|
this.once(type, () => {
|
|
93
177
|
resolve({ value: this, done: false });
|
|
@@ -107,46 +191,16 @@ export abstract class Input<Type>
|
|
|
107
191
|
}
|
|
108
192
|
|
|
109
193
|
/**
|
|
110
|
-
* Render a
|
|
194
|
+
* Render a debugging string.
|
|
111
195
|
*/
|
|
112
196
|
public toString(): string {
|
|
113
197
|
return `${this[InputIcon]} [${this.active ? "X" : "_"}]`;
|
|
114
198
|
}
|
|
115
199
|
|
|
116
|
-
constructor(params?: InputParams) {
|
|
117
|
-
super();
|
|
118
|
-
|
|
119
|
-
const { icon, name, parent, threshold } = {
|
|
120
|
-
...InputDefaults,
|
|
121
|
-
...(params || {}),
|
|
122
|
-
};
|
|
123
|
-
this[InputName] = name;
|
|
124
|
-
this[InputIcon] = icon;
|
|
125
|
-
this[InputParent] = parent;
|
|
126
|
-
|
|
127
|
-
this.threshold = threshold;
|
|
128
|
-
this.id = Symbol(this[InputName]);
|
|
129
|
-
|
|
130
|
-
this[InputChanged] = this[InputSetComparison]();
|
|
131
|
-
setImmediate(() => {
|
|
132
|
-
this[InputAdopt]();
|
|
133
|
-
this[InputChanged] = this[InputSetComparison]();
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
200
|
/**
|
|
138
|
-
*
|
|
201
|
+
* Returns true if the provided state is worth an event
|
|
139
202
|
*/
|
|
140
|
-
[
|
|
141
|
-
|
|
142
|
-
// TODO Support params for nested inputs
|
|
143
|
-
[inspect.custom](): string {
|
|
144
|
-
return `${this[InputName]} ${this[InputIcon]}: ${JSON.stringify(
|
|
145
|
-
this.state instanceof Input && this.state.id === this.id
|
|
146
|
-
? "virtual"
|
|
147
|
-
: this.state
|
|
148
|
-
)}`;
|
|
149
|
-
}
|
|
203
|
+
[InputComparator]: (state: Type, newState: Type) => boolean = BasicComparator;
|
|
150
204
|
|
|
151
205
|
[Symbol.asyncIterator](): AsyncIterator<this> {
|
|
152
206
|
return this;
|
|
@@ -168,62 +222,32 @@ export abstract class Input<Type>
|
|
|
168
222
|
readonly [InputName]: string;
|
|
169
223
|
|
|
170
224
|
/**
|
|
171
|
-
* A short name for this input
|
|
225
|
+
* A short name for this input.
|
|
172
226
|
*/
|
|
173
227
|
readonly [InputIcon]: string;
|
|
174
228
|
|
|
175
229
|
/**
|
|
176
|
-
*
|
|
230
|
+
* Other Inputs that contain this one.
|
|
177
231
|
*/
|
|
178
|
-
[
|
|
179
|
-
|
|
180
|
-
[InputChildless]: boolean = true;
|
|
232
|
+
private [InputParents] = new Set<Input<unknown>>();
|
|
181
233
|
|
|
182
234
|
/**
|
|
183
|
-
*
|
|
184
|
-
* And decide if this is the root Input.
|
|
235
|
+
* Links Inputs to bubble up events.
|
|
185
236
|
*/
|
|
186
|
-
[InputAdopt](): void {
|
|
187
|
-
|
|
188
|
-
if (value === this) return;
|
|
189
|
-
if (value instanceof Input) {
|
|
190
|
-
this[InputChildless] = false;
|
|
191
|
-
if (!value[InputChildless]) return;
|
|
192
|
-
value.on("change", (that, value) => {
|
|
193
|
-
that;
|
|
194
|
-
this.emit("change", this, value);
|
|
195
|
-
});
|
|
196
|
-
value.on("input", (that, value) => {
|
|
197
|
-
that;
|
|
198
|
-
this.emit("input", this, value);
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
});
|
|
237
|
+
[InputAdopt](parent: Input<unknown>): void {
|
|
238
|
+
this[InputParents].add(parent);
|
|
202
239
|
}
|
|
203
240
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
[InputChangedPrimitive](state: Type, newState: Type): boolean {
|
|
209
|
-
return state !== newState;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
[InputChangedThreshold](state: number, newState: number): boolean {
|
|
213
|
-
return Math.abs(state - newState) > this.threshold;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Sets a default comparison type for the Input based on the generic type.
|
|
217
|
-
[InputSetComparison](): (state: Type, newState: Type) => boolean {
|
|
241
|
+
/**
|
|
242
|
+
* Sets a default comparison type for the Input by discovering the generic type.
|
|
243
|
+
*/
|
|
244
|
+
[InputSetComparator](): void {
|
|
218
245
|
if (typeof this.state === "number") {
|
|
219
|
-
|
|
220
|
-
state: Type,
|
|
221
|
-
newState: Type
|
|
222
|
-
) => boolean;
|
|
246
|
+
this[InputComparator] = ThresholdComparator.bind(this, this.threshold);
|
|
223
247
|
} else if (this.state instanceof Input) {
|
|
224
|
-
|
|
248
|
+
this[InputComparator] = VirtualComparator;
|
|
225
249
|
} else {
|
|
226
|
-
|
|
250
|
+
this[InputComparator] = BasicComparator;
|
|
227
251
|
}
|
|
228
252
|
}
|
|
229
253
|
|
|
@@ -231,18 +255,12 @@ export abstract class Input<Type>
|
|
|
231
255
|
* Update the input's state and trigger all necessary callbacks.
|
|
232
256
|
*/
|
|
233
257
|
[InputSet](state: Type): void {
|
|
234
|
-
this.
|
|
235
|
-
if (this[InputChanged](this.state, state)) {
|
|
258
|
+
if (this[InputComparator](this.state, state)) {
|
|
236
259
|
this.state = state;
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
state
|
|
241
|
-
? this.emit("press", this, this)
|
|
242
|
-
: this.emit("release", this, this);
|
|
243
|
-
}
|
|
260
|
+
this.emit("change", this);
|
|
261
|
+
if (typeof state === "boolean")
|
|
262
|
+
this.emit(state ? "press" : "release", this);
|
|
244
263
|
}
|
|
245
|
-
|
|
246
|
-
this.emit("input", this, this);
|
|
264
|
+
this.emit("input", this);
|
|
247
265
|
}
|
|
248
266
|
}
|
package/src/readme.spec.ts
CHANGED
|
@@ -21,7 +21,7 @@ describe("README.md example snippets", () => {
|
|
|
21
21
|
|
|
22
22
|
it("should support callbacks", (done) => {
|
|
23
23
|
expect(controller.triangle.active).toEqual(false);
|
|
24
|
-
|
|
24
|
+
setTimeout(() => {
|
|
25
25
|
controller.triangle[InputSet](true);
|
|
26
26
|
});
|
|
27
27
|
|
|
@@ -29,10 +29,8 @@ describe("README.md example snippets", () => {
|
|
|
29
29
|
expect(input.active).toEqual(true);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
controller.triangle.removeAllListeners();
|
|
33
|
-
|
|
34
32
|
expect(controller.dpad.up.active).toEqual(false);
|
|
35
|
-
|
|
33
|
+
setTimeout(() => {
|
|
36
34
|
controller.dpad.up[InputSet](true);
|
|
37
35
|
});
|
|
38
36
|
|
|
@@ -44,13 +42,13 @@ describe("README.md example snippets", () => {
|
|
|
44
42
|
});
|
|
45
43
|
|
|
46
44
|
it("should provide promises", async () => {
|
|
47
|
-
|
|
45
|
+
setTimeout(() => {
|
|
48
46
|
controller.dpad.up[InputSet](true);
|
|
49
47
|
});
|
|
50
48
|
const { active } = await controller.dpad.up.promise();
|
|
51
49
|
expect(active).toEqual(true);
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
setTimeout(() => {
|
|
54
52
|
controller.dpad.up[InputSet](false);
|
|
55
53
|
});
|
|
56
54
|
const { left, up, down, right } = await controller.dpad.promise();
|
|
@@ -64,7 +62,7 @@ describe("README.md example snippets", () => {
|
|
|
64
62
|
let state = true;
|
|
65
63
|
let iterations = 5;
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
setTimeout(() => {
|
|
68
66
|
controller.dpad.up[InputSet](state);
|
|
69
67
|
});
|
|
70
68
|
for await (const { left, right, up, down } of controller.dpad) {
|
|
@@ -75,7 +73,7 @@ describe("README.md example snippets", () => {
|
|
|
75
73
|
iterations--;
|
|
76
74
|
state = !state;
|
|
77
75
|
if (iterations === 0) break;
|
|
78
|
-
|
|
76
|
+
setTimeout(() => {
|
|
79
77
|
controller.dpad.up[InputSet](state);
|
|
80
78
|
});
|
|
81
79
|
}
|