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.
Files changed (96) hide show
  1. package/README.md +9 -2
  2. package/dist/comparators.d.ts +13 -0
  3. package/dist/comparators.d.ts.map +1 -0
  4. package/dist/comparators.js +27 -0
  5. package/dist/comparators.js.map +1 -0
  6. package/dist/dualsense.d.ts +10 -1
  7. package/dist/dualsense.d.ts.map +1 -1
  8. package/dist/dualsense.js +46 -45
  9. package/dist/dualsense.js.map +1 -1
  10. package/dist/elements/analog.d.ts +12 -7
  11. package/dist/elements/analog.d.ts.map +1 -1
  12. package/dist/elements/analog.js +29 -11
  13. package/dist/elements/analog.js.map +1 -1
  14. package/dist/elements/axis.d.ts +22 -1
  15. package/dist/elements/axis.d.ts.map +1 -1
  16. package/dist/elements/axis.js +24 -4
  17. package/dist/elements/axis.js.map +1 -1
  18. package/dist/elements/dpad.d.ts +1 -1
  19. package/dist/elements/dpad.d.ts.map +1 -1
  20. package/dist/elements/dpad.js +6 -5
  21. package/dist/elements/dpad.js.map +1 -1
  22. package/dist/elements/touch.d.ts +2 -1
  23. package/dist/elements/touch.d.ts.map +1 -1
  24. package/dist/elements/touch.js +1 -0
  25. package/dist/elements/touch.js.map +1 -1
  26. package/dist/elements/touchpad.d.ts +1 -1
  27. package/dist/elements/touchpad.d.ts.map +1 -1
  28. package/dist/elements/touchpad.js.map +1 -1
  29. package/dist/elements/trigger.d.ts.map +1 -1
  30. package/dist/elements/trigger.js +1 -1
  31. package/dist/elements/trigger.js.map +1 -1
  32. package/dist/elements/unisense.d.ts +3 -3
  33. package/dist/elements/unisense.d.ts.map +1 -1
  34. package/dist/elements/unisense.js +13 -5
  35. package/dist/elements/unisense.js.map +1 -1
  36. package/dist/hid/dualsense_hid.d.ts +16 -58
  37. package/dist/hid/dualsense_hid.d.ts.map +1 -1
  38. package/dist/hid/dualsense_hid.js +25 -101
  39. package/dist/hid/dualsense_hid.js.map +1 -1
  40. package/dist/hid/hid_provider.d.ts +86 -0
  41. package/dist/hid/hid_provider.d.ts.map +1 -0
  42. package/dist/hid/hid_provider.js +45 -0
  43. package/dist/hid/hid_provider.js.map +1 -0
  44. package/dist/hid/index.d.ts +4 -1
  45. package/dist/hid/index.d.ts.map +1 -1
  46. package/dist/hid/index.js +4 -1
  47. package/dist/hid/index.js.map +1 -1
  48. package/dist/hid/node_hid_provider.d.ts +11 -0
  49. package/dist/hid/node_hid_provider.d.ts.map +1 -0
  50. package/dist/hid/node_hid_provider.js +97 -0
  51. package/dist/hid/node_hid_provider.js.map +1 -0
  52. package/dist/hid/platform_hid_provider.d.ts +4 -0
  53. package/dist/hid/platform_hid_provider.d.ts.map +1 -0
  54. package/dist/hid/platform_hid_provider.js +7 -0
  55. package/dist/hid/platform_hid_provider.js.map +1 -0
  56. package/dist/hid/web_hid_provider.d.ts +10 -0
  57. package/dist/hid/web_hid_provider.d.ts.map +1 -0
  58. package/dist/hid/web_hid_provider.js +102 -0
  59. package/dist/hid/web_hid_provider.js.map +1 -0
  60. package/dist/{hid/ids.d.ts → id.d.ts} +6 -2
  61. package/dist/id.d.ts.map +1 -0
  62. package/dist/{hid/ids.js → id.js} +1 -1
  63. package/dist/id.js.map +1 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/input.d.ts +63 -45
  69. package/dist/input.d.ts.map +1 -1
  70. package/dist/input.js +123 -90
  71. package/dist/input.js.map +1 -1
  72. package/package.json +14 -8
  73. package/src/comparators.ts +26 -0
  74. package/src/dualsense.ts +61 -58
  75. package/src/elements/analog.spec.ts +2 -15
  76. package/src/elements/analog.ts +41 -22
  77. package/src/elements/axis.ts +35 -3
  78. package/src/elements/dpad.ts +7 -6
  79. package/src/elements/touch.ts +2 -1
  80. package/src/elements/touchpad.ts +1 -1
  81. package/src/elements/trigger.ts +1 -1
  82. package/src/elements/unisense.ts +19 -17
  83. package/src/hid/dualsense_hid.ts +25 -156
  84. package/src/hid/{dualsense_hid.spec.ts → hid_provider.spec.ts} +1 -1
  85. package/src/hid/hid_provider.ts +100 -0
  86. package/src/hid/index.ts +4 -1
  87. package/src/hid/node_hid_provider.ts +108 -0
  88. package/src/hid/platform_hid_provider.ts +4 -0
  89. package/src/hid/web_hid_provider.ts +116 -0
  90. package/src/{hid/ids.ts → id.ts} +6 -1
  91. package/src/index.ts +2 -0
  92. package/src/input.ts +156 -138
  93. package/src/readme.spec.ts +6 -8
  94. package/webpack.config.js +42 -0
  95. package/dist/hid/ids.d.ts.map +0 -1
  96. 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,4 @@
1
+ import { NodeHIDProvider } from './node_hid_provider'
2
+ import { WebHIDProvider } from './web_hid_provider'
3
+
4
+ export const PlatformHIDProvider = typeof window === 'undefined' ? NodeHIDProvider : WebHIDProvider;
@@ -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
+ }
@@ -1,4 +1,6 @@
1
- // Names for real controller inputs
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
@@ -2,7 +2,9 @@
2
2
  * @file Automatically generated by barrelsby.
3
3
  */
4
4
 
5
+ export * from "./comparators";
5
6
  export * from "./dualsense";
7
+ export * from "./id";
6
8
  export * from "./input";
7
9
  export * from "./math";
8
10
  export * from "./elements/index";
package/src/input.ts CHANGED
@@ -1,93 +1,177 @@
1
- import { EventEmitter } from "events";
2
- import { inspect } from "util";
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
- const InputDefaults: Omit<Required<InputParams>, "parent"> = {
12
- name: "???",
13
- icon: "???",
14
- threshold: 0,
15
- };
16
+ export type InputChangeType = "change" | "press" | "release";
17
+ export type InputEventType = InputChangeType | "input";
16
18
 
17
- // Private properties
18
- const InputAdopt = Symbol("InputAdopt");
19
- const InputChildless = Symbol("InputChildless");
20
- const InputParent = Symbol("InputParent");
21
- const InputSetComparison = Symbol("InputSetComparison");
22
- const InputChangedPrimitive = Symbol("InputChangedPrimitive");
23
- const InputChangedThreshold = Symbol("InputChangedThreshold");
24
- const InputChangedVirtual = Symbol("InputChangedVirtual");
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
- // Utilities
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
- export type InputEvent = "change" | "input" | "press" | "release";
35
-
36
- export declare interface Input<Type> {
37
- on(
38
- event: InputEvent,
39
- listener: (
40
- input: Input<Type>,
41
- changed: Input<unknown>
42
- ) => unknown | Promise<unknown>
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
- extends EventEmitter
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
- * Timestamp of the last received input that changed the state.
52
+ * For numeric inputs, ignore state changes smaller than this threshold.
62
53
  */
63
- public lastChange: number = Date.now();
54
+ public threshold: number = 0;
64
55
 
65
56
  /**
66
- * Timestamp of the last received input, even if it didn't change the state.
57
+ * Provide the type and default value for the input.
67
58
  */
68
- public lastInput: number = Date.now();
59
+ public abstract state: Type;
69
60
 
70
61
  /**
71
- * For numeric inputs, ignore state changes smaller than this threshold.
62
+ * Stores event listeners.
72
63
  */
73
- public threshold: number = 0;
64
+ private [InputOns] = new Map<InputEventType, InputCallback<this>[]>();
74
65
 
75
66
  /**
76
- * Provide the type and default value for the input.
67
+ * Stores callbacks waiting for one-time events.
77
68
  */
78
- public abstract state: Type;
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 convenient debugging string.
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
- * Optionally, implement a function that returns true if the provided state is worth an event
201
+ * Returns true if the provided state is worth an event
139
202
  */
140
- [InputChanged]: (state: Type, newState: Type) => boolean;
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
- * The Input's parent, if any
230
+ * Other Inputs that contain this one.
177
231
  */
178
- [InputParent]?: Input<unknown>;
179
-
180
- [InputChildless]: boolean = true;
232
+ private [InputParents] = new Set<Input<unknown>>();
181
233
 
182
234
  /**
183
- * Cascade events from nested Inputs.
184
- * And decide if this is the root Input.
235
+ * Links Inputs to bubble up events.
185
236
  */
186
- [InputAdopt](): void {
187
- Object.values(this).forEach((value) => {
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
- [InputChangedVirtual](): boolean {
205
- return true;
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
- return this[InputChangedThreshold] as unknown as (
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
- return this[InputChangedVirtual];
248
+ this[InputComparator] = VirtualComparator;
225
249
  } else {
226
- return this[InputChangedPrimitive];
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.lastInput = Date.now();
235
- if (this[InputChanged](this.state, state)) {
258
+ if (this[InputComparator](this.state, state)) {
236
259
  this.state = state;
237
- this.lastChange = Date.now();
238
- this.emit("change", this, this);
239
- if (typeof state === "boolean") {
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
  }
@@ -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
- setImmediate(() => {
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
- setImmediate(() => {
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
- setImmediate(() => {
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
- setImmediate(() => {
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
- setImmediate(() => {
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
- setImmediate(() => {
76
+ setTimeout(() => {
79
77
  controller.dpad.up[InputSet](state);
80
78
  });
81
79
  }