A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using Unity.Collections;
4using UnityEngine.InputSystem.LowLevel;
5using UnityEngine.InputSystem.Utilities;
6using Unity.Profiling;
7
8////REVIEW: remove users automatically when exiting play mode?
9
10////REVIEW: do we need to handle the case where devices are added to a user that are each associated with a different user account
11
12////REVIEW: how should we handle pairings of devices *not* called for by a control scheme? should that result in a failed match?
13
14////TODO: option to bind to *all* devices instead of just the paired ones (bindToAllDevices)
15
16////TODO: the account selection stuff needs cleanup; the current flow is too convoluted
17
18namespace UnityEngine.InputSystem.Users
19{
20 /// <summary>
21 /// Represents a specific user/player interacting with one or more devices and input actions.
22 /// </summary>
23 /// <remarks>
24 /// Principally, an InputUser represents a human interacting with the application. Moreover, at any point
25 /// each InputUser represents a human actor distinct from all other InputUsers in the system.
26 ///
27 /// Each user has one or more paired devices. In general, these devices are unique to each user. However,
28 /// it is permitted to use <see cref="PerformPairingWithDevice"/> to pair the same device to multiple users.
29 /// This can be useful in setups such as split-keyboard (e.g. one user using left side of keyboard and the
30 /// other the right one) use or hotseat-style gameplay (e.g. two players taking turns on the same game controller).
31 ///
32 /// Note that the InputUser API, like <see cref="InputAction"/>) is a play mode-only feature. When exiting play mode,
33 /// all users are automatically removed and all devices automatically unpaired.
34 /// </remarks>
35 /// <seealso cref="InputUserChange"/>
36 public struct InputUser : IEquatable<InputUser>
37 {
38 public const uint InvalidId = 0;
39
40 static readonly ProfilerMarker k_InputUserOnChangeMarker = new ProfilerMarker("InputUser.onChange");
41 static readonly ProfilerMarker k_InputCheckForUnpairMarker = new ProfilerMarker("InputCheckForUnpairedDeviceActivity");
42
43 /// <summary>
44 /// Whether this is a currently active user record in <see cref="all"/>.
45 /// </summary>
46 /// <remarks>
47 /// Users that are removed (<see cref="UnpairDevicesAndRemoveUser"/>) will become invalid.
48 /// </remarks>
49 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
50 /// <seealso cref="InputUserChange.Removed"/>
51 public bool valid
52 {
53 get
54 {
55 if (m_Id == InvalidId)
56 return false;
57
58 // See if there's a currently active user with the given ID.
59 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
60 if (s_GlobalState.allUsers[i].m_Id == m_Id)
61 return true;
62
63 return false;
64 }
65 }
66
67 /// <summary>
68 /// The sequence number of the user.
69 /// </summary>
70 /// <remarks>
71 /// It can be useful to establish a sorting of players locally such that it is
72 /// known who is the first player, who is the second, and so on. This property
73 /// gives the positioning of the user within <see cref="all"/>.
74 ///
75 /// Note that the index of a user may change as users are added and removed.
76 /// </remarks>
77 /// <seealso cref="all"/>
78 public int index
79 {
80 get
81 {
82 if (m_Id == InvalidId)
83 throw new InvalidOperationException("Invalid user");
84
85 var userIndex = TryFindUserIndex(m_Id);
86 if (userIndex == -1)
87 throw new InvalidOperationException($"User with ID {m_Id} is no longer valid");
88
89 return userIndex;
90 }
91 }
92
93 /// <summary>
94 /// The unique numeric ID of the user.
95 /// </summary>
96 /// <remarks>
97 /// The ID of a user is internally assigned and cannot be changed over its lifetime. No two users, even
98 /// if not concurrently active, will receive the same ID.
99 ///
100 /// The ID stays valid and unique even if the user is removed and no longer <see cref="valid"/>.
101 /// </remarks>
102 public uint id => m_Id;
103
104 ////TODO: bring documentation for these back when user management is implemented on Xbox and PS
105 public InputUserAccountHandle? platformUserAccountHandle => s_GlobalState.allUserData[index].platformUserAccountHandle;
106 public string platformUserAccountName => s_GlobalState.allUserData[index].platformUserAccountName;
107 public string platformUserAccountId => s_GlobalState.allUserData[index].platformUserAccountId;
108
109 ////REVIEW: Does it make sense to track used devices separately from paired devices?
110 /// <summary>
111 /// Devices assigned/paired/linked to the user.
112 /// </summary>
113 /// <remarks>
114 /// It is generally valid for a device to be assigned to multiple users. For example, two users could
115 /// both use the local keyboard in a split-keyboard or hot seat setup. However, a platform may restrict this
116 /// and mandate that a device never belong to more than one user. This is the case on Xbox and PS4, for
117 /// example.
118 ///
119 /// To associate devices with users, use <see cref="PerformPairingWithDevice"/>. To remove devices, use
120 /// <see cref="UnpairDevice"/> or <see cref="UnpairDevicesAndRemoveUser"/>.
121 ///
122 /// The array will be empty for a user who is currently not paired to any devices.
123 ///
124 /// If <see cref="actions"/> is set (<see cref="AssociateActionsWithUser(IInputActionCollection)"/>), then
125 /// <see cref="IInputActionCollection.devices"/> will be kept synchronized with the devices paired to the user.
126 /// </remarks>
127 /// <seealso cref="PerformPairingWithDevice"/>
128 /// <seealso cref="UnpairDevice"/>
129 /// <seealso cref="UnpairDevices"/>
130 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
131 /// <seealso cref="InputUserChange.DevicePaired"/>
132 /// <seealso cref="InputUserChange.DeviceUnpaired"/>
133 public ReadOnlyArray<InputDevice> pairedDevices
134 {
135 get
136 {
137 var userIndex = index;
138 return new ReadOnlyArray<InputDevice>(s_GlobalState.allPairedDevices, s_GlobalState.allUserData[userIndex].deviceStartIndex,
139 s_GlobalState.allUserData[userIndex].deviceCount);
140 }
141 }
142
143 /// <summary>
144 /// Devices that were removed while they were still paired to the user.
145 /// </summary>
146 /// <remarks>
147 ///
148 /// This list is cleared once the user has either regained lost devices or has regained other devices
149 /// such that the <see cref="controlScheme"/> is satisfied.
150 /// </remarks>
151 /// <seealso cref="InputUserChange.DeviceRegained"/>
152 /// <seealso cref="InputUserChange.DeviceLost"/>
153 public ReadOnlyArray<InputDevice> lostDevices
154 {
155 get
156 {
157 var userIndex = index;
158 return new ReadOnlyArray<InputDevice>(s_GlobalState.allLostDevices, s_GlobalState.allUserData[userIndex].lostDeviceStartIndex,
159 s_GlobalState.allUserData[userIndex].lostDeviceCount);
160 }
161 }
162
163
164 /// <summary>
165 /// Actions associated with the user.
166 /// </summary>
167 /// <remarks>
168 /// Associating actions with a user will synchronize the actions with the devices paired to the
169 /// user. Also, it makes it possible to use support for control scheme activation (<see
170 /// cref="ActivateControlScheme(InputControlScheme)"/> and related APIs like <see cref="controlScheme"/>
171 /// and <see cref="controlSchemeMatch"/>).
172 ///
173 /// Note that is generally does not make sense for users to share actions. Instead, each user should
174 /// receive a set of actions private to the user.
175 /// </remarks>
176 /// <seealso cref="AssociateActionsWithUser(IInputActionCollection)"/>
177 /// <seealso cref="InputActionMap"/>
178 /// <seealso cref="InputActionAsset"/>
179 /// <seealso cref="InputUserChange.ControlsChanged"/>
180 public IInputActionCollection actions => s_GlobalState.allUserData[index].actions;
181
182 /// <summary>
183 /// The control scheme currently employed by the user.
184 /// </summary>
185 /// <remarks>
186 /// This is null by default.
187 ///
188 /// Any time the value of this property changes (whether by <see cref="ActivateControlScheme(string)"/>
189 /// or by automatic switching), a notification is sent on <see cref="onChange"/> with
190 /// <see cref="InputUserChange.ControlSchemeChanged"/>.
191 ///
192 /// Be aware that using control schemes with InputUsers requires <see cref="actions"/> to
193 /// be set, i.e. input actions to be associated with the user (<see
194 /// cref="AssociateActionsWithUser(IInputActionCollection)"/>).
195 /// </remarks>
196 /// <seealso cref="ActivateControlScheme(string)"/>
197 /// <seealso cref="ActivateControlScheme(InputControlScheme)"/>
198 /// <seealso cref="InputUserChange.ControlSchemeChanged"/>
199 public InputControlScheme? controlScheme => s_GlobalState.allUserData[index].controlScheme;
200
201 /// <summary>
202 /// The result of matching the device requirements given by <see cref="controlScheme"/> against
203 /// the devices paired to the user (<see cref="pairedDevices"/>).
204 /// </summary>
205 /// <remarks>
206 /// When devices are paired to or unpaired from a user, as well as when a new control scheme is
207 /// activated on a user, this property is updated automatically.
208 /// </remarks>
209 /// <seealso cref="InputControlScheme.deviceRequirements"/>
210 /// <seealso cref="InputControlScheme.PickDevicesFrom{TDevices}"/>
211 public InputControlScheme.MatchResult controlSchemeMatch => s_GlobalState.allUserData[index].controlSchemeMatch;
212
213 /// <summary>
214 /// Whether the user is missing devices required by the <see cref="controlScheme"/> activated
215 /// on the user.
216 /// </summary>
217 /// <remarks>
218 /// This will only take required devices into account. Device requirements marked optional (<see
219 /// cref="InputControlScheme.DeviceRequirement.isOptional"/>) will not be considered missing
220 /// devices if they cannot be satisfied based on the devices paired to the user.
221 /// </remarks>
222 /// <seealso cref="InputControlScheme.deviceRequirements"/>
223 public bool hasMissingRequiredDevices => s_GlobalState.allUserData[index].controlSchemeMatch.hasMissingRequiredDevices;
224
225 /// <summary>
226 /// List of all current users.
227 /// </summary>
228 /// <remarks>
229 /// Use <see cref="PerformPairingWithDevice"/> to add new users and <see cref="UnpairDevicesAndRemoveUser"/> to
230 /// remove users.
231 ///
232 /// Note that this array does not necessarily correspond to the list of users present at the platform level
233 /// (e.g. Xbox and PS4). There can be users present at the platform level that are not present in this array
234 /// (e.g. because they are not joined to the game) and users can even be present more than once (e.g. if
235 /// playing on the user account but as two different players in the game). Also, there can be users in the array
236 /// that are not present at the platform level.
237 /// </remarks>
238 /// <seealso cref="PerformPairingWithDevice"/>
239 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
240 public static ReadOnlyArray<InputUser> all => new ReadOnlyArray<InputUser>(s_GlobalState.allUsers, 0, s_GlobalState.allUserCount);
241
242 /// <summary>
243 /// Event that is triggered when the <see cref="InputUser">user</see> setup in the system
244 /// changes.
245 /// </summary>
246 /// <remarks>
247 /// Each notification receives the user that was affected by the change and, in the form of <see cref="InputUserChange"/>,
248 /// a description of what has changed about the user. The third parameter may be null but if the change will be related
249 /// to an input device, will reference the device involved in the change.
250 /// </remarks>
251 public static event Action<InputUser, InputUserChange, InputDevice> onChange
252 {
253 add
254 {
255 if (value == null)
256 throw new ArgumentNullException(nameof(value));
257 s_GlobalState.onChange.AddCallback(value);
258 }
259 remove
260 {
261 if (value == null)
262 throw new ArgumentNullException(nameof(value));
263 s_GlobalState.onChange.RemoveCallback(value);
264 }
265 }
266
267 /// <summary>
268 /// Event that is triggered when a device is used that is not currently paired to any user.
269 /// </summary>
270 /// <remarks>
271 /// A device is considered "used" when it has magnitude (<see cref="InputControl.EvaluateMagnitude()"/>) greater than zero
272 /// on a control that is not noisy (<see cref="InputControl.noisy"/>) and not synthetic (i.e. not a control that is
273 /// "made up" like <see cref="Keyboard.anyKey"/>; <see cref="InputControl.synthetic"/>).
274 ///
275 /// Detecting the use of unpaired devices has a non-zero cost. While multiple levels of tests are applied to try to
276 /// cheaply ignore devices that have events sent to them that do not contain user activity, finding out whether
277 /// a device had real user activity will eventually require going through the device control by control.
278 ///
279 /// To enable detection of the use of unpaired devices, set <see cref="listenForUnpairedDeviceActivity"/> to true.
280 /// It is disabled by default.
281 ///
282 /// The callback is invoked for each non-leaf, non-synthetic, non-noisy control that has been actuated on the device.
283 /// It being restricted to non-leaf controls means that if, say, the stick on a gamepad is actuated in both X and Y
284 /// direction, you will see two calls: one with stick/x and one with stick/y.
285 ///
286 /// The reason that the callback is invoked for each individual control is that pairing often relies on checking
287 /// for specific kinds of interactions. For example, a pairing callback may listen exclusively for button presses.
288 ///
289 /// Note that whether the use of unpaired devices leads to them getting paired is under the control of the application.
290 /// If the device should be paired, invoke <see cref="PerformPairingWithDevice"/> from the callback. If you do so,
291 /// no further callbacks will get triggered for other controls that may have been actuated in the same event.
292 ///
293 /// Be aware that the callback is fired <em>before</em> input is actually incorporated into the device (it is
294 /// indirectly triggered from <see cref="InputSystem.onEvent"/>). This means at the time the callback is run,
295 /// the state of the given device does not yet have the input that triggered the callback. For this reason, the
296 /// callback receives a second argument that references the event from which the use of an unpaired device was
297 /// detected.
298 ///
299 /// What this sequence allows is to make changes to the system before the input is processed. For example, an
300 /// action that is enabled as part of the callback will subsequently respond to the input that triggered the
301 /// callback.
302 ///
303 /// <example>
304 /// <code>
305 /// // Activate support for listening to device activity.
306 /// ++InputUser.listenForUnpairedDeviceActivity;
307 ///
308 /// // When a button on an unpaired device is pressed, pair the device to a new
309 /// // or existing user.
310 /// InputUser.onUnpairedDeviceUsed +=
311 /// usedControl =>
312 /// {
313 /// // Only react to button presses on unpaired devices.
314 /// if (!(usedControl is ButtonControl))
315 /// return;
316 ///
317 /// // Pair the device to a user.
318 /// InputUser.PerformPairingWithDevice(usedControl.device);
319 /// };
320 /// </code>
321 /// </example>
322 ///
323 /// Another possible use of the callback is for implementing automatic control scheme switching for a user such that
324 /// the user can, for example, switch from keyboard&mouse to gamepad seamlessly by simply picking up the gamepad
325 /// and starting to play.
326 /// </remarks>
327 public static event Action<InputControl, InputEventPtr> onUnpairedDeviceUsed
328 {
329 add
330 {
331 if (value == null)
332 throw new ArgumentNullException(nameof(value));
333 s_GlobalState.onUnpairedDeviceUsed.AddCallback(value);
334 if (s_GlobalState.listenForUnpairedDeviceActivity > 0)
335 HookIntoEvents();
336 }
337 remove
338 {
339 if (value == null)
340 throw new ArgumentNullException(nameof(value));
341 s_GlobalState.onUnpairedDeviceUsed.RemoveCallback(value);
342 if (s_GlobalState.onUnpairedDeviceUsed.length == 0)
343 UnhookFromDeviceStateChange();
344 }
345 }
346
347 /// <summary>
348 /// Callback that works in combination with <see cref="onUnpairedDeviceUsed"/>. If all callbacks
349 /// added to this event return <c>false</c> for a
350 /// </summary>
351 /// <remarks>
352 /// Checking a given event for activity of interest is relatively fast but is still costlier than
353 /// not doing it all. In case only certain devices are of interest for <see cref="onUnpairedDeviceUsed"/>,
354 /// this "pre-filter" can be used to quickly reject entire devices and thus skip looking closer at
355 /// an event.
356 ///
357 /// The first argument is the <see cref="InputDevice"/> than an event has been received for.
358 /// The second argument is the <see cref="InputEvent"/> that is being looked at.
359 ///
360 /// A callback should return <c>true</c> if it wants <see cref="onUnpairedDeviceUsed"/> to proceed
361 /// looking at the event and should return <c>false</c> if the event should be skipped.
362 ///
363 /// If multiple callbacks are added to the event, it is enough for any single one callback
364 /// to return <c>true</c> for the event to get looked at.
365 /// </remarks>
366 /// <seealso cref="onUnpairedDeviceUsed"/>
367 /// <seealso cref="listenForUnpairedDeviceActivity"/>
368 public static event Func<InputDevice, InputEventPtr, bool> onPrefilterUnpairedDeviceActivity
369 {
370 add
371 {
372 if (value == null)
373 throw new ArgumentNullException(nameof(value));
374 s_GlobalState.onPreFilterUnpairedDeviceUsed.AddCallback(value);
375 }
376 remove
377 {
378 if (value == null)
379 throw new ArgumentNullException(nameof(value));
380 s_GlobalState.onPreFilterUnpairedDeviceUsed.RemoveCallback(value);
381 }
382 }
383
384 ////TODO: After 1.0, make this a simple bool API that *underneath* uses a counter rather than exposing the counter
385 //// directly to the user.
386 /// <summary>
387 /// Whether to listen for user activity on currently unpaired devices and invoke <see cref="onUnpairedDeviceUsed"/>
388 /// if such activity is detected.
389 /// </summary>
390 /// <remarks>
391 /// This is off by default.
392 ///
393 /// Note that enabling this has a non-zero cost. Whenever the state changes of a device that is not currently paired
394 /// to a user, the system has to spend time figuring out whether there was a meaningful change or whether it's just
395 /// noise on the device.
396 ///
397 /// This is an integer rather than a bool to allow multiple systems to concurrently use to listen for unpaired
398 /// device activity without treading on each other when enabling/disabling the code path.
399 /// </remarks>
400 /// <seealso cref="onUnpairedDeviceUsed"/>
401 /// <seealso cref="pairedDevices"/>
402 /// <seealso cref="PerformPairingWithDevice"/>
403 public static int listenForUnpairedDeviceActivity
404 {
405 get => s_GlobalState.listenForUnpairedDeviceActivity;
406 set
407 {
408 if (value < 0)
409 throw new ArgumentOutOfRangeException(nameof(value), "Cannot be negative");
410 if (value > 0 && s_GlobalState.onUnpairedDeviceUsed.length > 0)
411 HookIntoEvents();
412 else if (value == 0)
413 UnhookFromDeviceStateChange();
414 s_GlobalState.listenForUnpairedDeviceActivity = value;
415 }
416 }
417
418 public override string ToString()
419 {
420 if (!valid)
421 return $"<Invalid> (id: {m_Id})";
422
423 var deviceList = string.Join(",", pairedDevices);
424 return $"User #{index} (id: {m_Id}, devices: {deviceList}, actions: {actions})";
425 }
426
427 /// <summary>
428 /// Associate a collection of <see cref="InputAction"/>s with the user.
429 /// </summary>
430 /// <param name="actions">Actions to associate with the user, either an <see cref="InputActionAsset"/>
431 /// or an <see cref="InputActionMap"/>. Can be <c>null</c> to unset the current association.</param>
432 /// <exception cref="InvalidOperationException">The user instance is invalid.</exception>
433 /// <remarks>
434 /// Associating actions with a user will ensure that the <see cref="IInputActionCollection.devices"/> and
435 /// <see cref="IInputActionCollection.bindingMask"/> property of the action collection are automatically
436 /// kept in sync with the device paired to the user (see <see cref="pairedDevices"/>) and the control
437 /// scheme active on the user (see <see cref="controlScheme"/>).
438 ///
439 /// <example>
440 /// <code>
441 /// var gamepad = Gamepad.all[0];
442 ///
443 /// // Pair the gamepad to a user.
444 /// var user = InputUser.PerformPairingWithDevice(gamepad);
445 ///
446 /// // Create an action map with an action.
447 /// var actionMap = new InputActionMap():
448 /// actionMap.AddAction("Fire", binding: "<Gamepad>/buttonSouth");
449 ///
450 /// // Associate the action map with the user (the same works for an asset).
451 /// user.AssociateActionsWithUser(actionMap);
452 ///
453 /// // Now the action map is restricted to just the gamepad that is paired
454 /// // with the user, even if there are more gamepads currently connected.
455 /// </code>
456 /// </example>
457 /// </remarks>
458 /// <seealso cref="actions"/>
459 public void AssociateActionsWithUser(IInputActionCollection actions)
460 {
461 var userIndex = index; // Throws if user is invalid.
462 if (s_GlobalState.allUserData[userIndex].actions == actions)
463 return;
464
465 // If we already had actions associated, reset the binding mask and device list.
466 var oldActions = s_GlobalState.allUserData[userIndex].actions;
467 if (oldActions != null)
468 {
469 oldActions.devices = null;
470 oldActions.bindingMask = null;
471 }
472
473 s_GlobalState.allUserData[userIndex].actions = actions;
474
475 // If we've switched to a different set of actions, synchronize our state.
476 if (actions != null)
477 {
478 HookIntoActionChange();
479
480 actions.devices = pairedDevices;
481 if (s_GlobalState.allUserData[userIndex].controlScheme != null)
482 ActivateControlSchemeInternal(userIndex, s_GlobalState.allUserData[userIndex].controlScheme.Value);
483 }
484 }
485
486 public ControlSchemeChangeSyntax ActivateControlScheme(string schemeName)
487 {
488 // Look up control scheme by name in actions.
489 if (!string.IsNullOrEmpty(schemeName))
490 {
491 FindControlScheme(schemeName, out InputControlScheme scheme); // throws if not found
492 return ActivateControlScheme(scheme);
493 }
494 return ActivateControlScheme(new InputControlScheme());
495 }
496
497 private bool TryFindControlScheme(string schemeName, out InputControlScheme scheme)
498 {
499 if (string.IsNullOrEmpty(schemeName))
500 {
501 scheme = default;
502 return false;
503 }
504
505 // Need actions to be available to be able to activate control schemes by name only.
506 if (s_GlobalState.allUserData[index].actions == null)
507 throw new InvalidOperationException(
508 $"Cannot set control scheme '{schemeName}' by name on user #{index} as not actions have been associated with the user yet (AssociateActionsWithUser)");
509
510 // Attempt to find control scheme by name
511 var controlSchemes = s_GlobalState.allUserData[index].actions.controlSchemes;
512 for (var i = 0; i < controlSchemes.Count; ++i)
513 {
514 if (string.Compare(controlSchemes[i].name, schemeName,
515 StringComparison.InvariantCultureIgnoreCase) == 0)
516 {
517 scheme = controlSchemes[i];
518 return true;
519 }
520 }
521
522 scheme = default;
523 return false;
524 }
525
526 internal void FindControlScheme(string schemeName, out InputControlScheme scheme)
527 {
528 if (TryFindControlScheme(schemeName, out scheme))
529 return;
530 throw new ArgumentException(
531 $"Cannot find control scheme '{schemeName}' in actions '{s_GlobalState.allUserData[index].actions}'");
532 }
533
534 public ControlSchemeChangeSyntax ActivateControlScheme(InputControlScheme scheme)
535 {
536 var userIndex = index; // Throws if user is invalid.
537
538 if (s_GlobalState.allUserData[userIndex].controlScheme != scheme ||
539 (scheme == default && s_GlobalState.allUserData[userIndex].controlScheme != null))
540 {
541 ActivateControlSchemeInternal(userIndex, scheme);
542 Notify(userIndex, InputUserChange.ControlSchemeChanged, null);
543 }
544
545 return new ControlSchemeChangeSyntax { m_UserIndex = userIndex };
546 }
547
548 private void ActivateControlSchemeInternal(int userIndex, InputControlScheme scheme)
549 {
550 var isEmpty = scheme == default;
551
552 if (isEmpty)
553 s_GlobalState.allUserData[userIndex].controlScheme = null;
554 else
555 s_GlobalState.allUserData[userIndex].controlScheme = scheme;
556
557 if (s_GlobalState.allUserData[userIndex].actions != null)
558 {
559 if (isEmpty)
560 {
561 s_GlobalState.allUserData[userIndex].actions.bindingMask = null;
562 s_GlobalState.allUserData[userIndex].controlSchemeMatch.Dispose();
563 s_GlobalState.allUserData[userIndex].controlSchemeMatch = new InputControlScheme.MatchResult();
564 }
565 else
566 {
567 s_GlobalState.allUserData[userIndex].actions.bindingMask = new InputBinding { groups = scheme.bindingGroup };
568 UpdateControlSchemeMatch(userIndex);
569
570 // If we had lost some devices, flush the list. We haven't regained the device
571 // but we're no longer missing devices to play.
572 if (s_GlobalState.allUserData[userIndex].controlSchemeMatch.isSuccessfulMatch)
573 RemoveLostDevicesForUser(userIndex);
574 }
575 }
576 }
577
578 /// <summary>
579 /// Unpair a single device from the user.
580 /// </summary>
581 /// <param name="device">Device to unpair from the user. If the device is not currently paired to the user,
582 /// the method does nothing.</param>
583 /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
584 /// <remarks>
585 /// If actions are associated with the user (<see cref="actions"/>), the list of devices used by the
586 /// actions (<see cref="IInputActionCollection.devices"/>) is automatically updated.
587 ///
588 /// If a control scheme is activated on the user (<see cref="controlScheme"/>), <see cref="controlSchemeMatch"/>
589 /// is automatically updated.
590 ///
591 /// Sends <see cref="InputUserChange.DeviceUnpaired"/> through <see cref="onChange"/>.
592 /// </remarks>
593 /// <seealso cref="PerformPairingWithDevice"/>
594 /// <seealso cref="pairedDevices"/>
595 /// <seealso cref="UnpairDevices"/>
596 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
597 /// <seealso cref="InputUserChange.DeviceUnpaired"/>
598 public void UnpairDevice(InputDevice device)
599 {
600 if (device == null)
601 throw new ArgumentNullException(nameof(device));
602
603 var userIndex = index; // Throws if user is invalid.
604
605 // Ignore if not currently paired to user.
606 if (!pairedDevices.ContainsReference(device))
607 return;
608
609 RemoveDeviceFromUser(userIndex, device);
610 }
611
612 /// <summary>
613 /// Unpair all devices from the user.
614 /// </summary>
615 /// <remarks>
616 /// If actions are associated with the user (<see cref="actions"/>), the list of devices used by the
617 /// actions (<see cref="IInputActionCollection.devices"/>) is automatically updated.
618 ///
619 /// If a control scheme is activated on the user (<see cref="controlScheme"/>), <see cref="controlSchemeMatch"/>
620 /// is automatically updated.
621 ///
622 /// Sends <see cref="InputUserChange.DeviceUnpaired"/> through <see cref="onChange"/> for every device
623 /// unpaired from the user.
624 /// </remarks>
625 /// <seealso cref="PerformPairingWithDevice"/>
626 /// <seealso cref="pairedDevices"/>
627 /// <seealso cref="UnpairDevice"/>
628 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
629 /// <seealso cref="InputUserChange.DeviceUnpaired"/>
630 public void UnpairDevices()
631 {
632 var userIndex = index; // Throws if user is invalid.
633
634 RemoveLostDevicesForUser(userIndex);
635
636 using (InputActionRebindingExtensions.DeferBindingResolution())
637 {
638 // We could remove the devices in bulk here but we still have to notify one
639 // by one which ends up being more complicated than just unpairing the devices
640 // individually here.
641 while (s_GlobalState.allUserData[userIndex].deviceCount > 0)
642 UnpairDevice(s_GlobalState.allPairedDevices[s_GlobalState.allUserData[userIndex].deviceStartIndex + s_GlobalState.allUserData[userIndex].deviceCount - 1]);
643 }
644
645 // Update control scheme, if necessary.
646 if (s_GlobalState.allUserData[userIndex].controlScheme != null)
647 UpdateControlSchemeMatch(userIndex);
648 }
649
650 private static void RemoveLostDevicesForUser(int userIndex)
651 {
652 var lostDeviceCount = s_GlobalState.allUserData[userIndex].lostDeviceCount;
653 if (lostDeviceCount > 0)
654 {
655 var lostDeviceStartIndex = s_GlobalState.allUserData[userIndex].lostDeviceStartIndex;
656 ArrayHelpers.EraseSliceWithCapacity(ref s_GlobalState.allLostDevices, ref s_GlobalState.allLostDeviceCount,
657 lostDeviceStartIndex, lostDeviceCount);
658
659 s_GlobalState.allUserData[userIndex].lostDeviceCount = 0;
660 s_GlobalState.allUserData[userIndex].lostDeviceStartIndex = 0;
661
662 // Adjust indices of other users.
663 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
664 {
665 if (s_GlobalState.allUserData[i].lostDeviceStartIndex > lostDeviceStartIndex)
666 s_GlobalState.allUserData[i].lostDeviceStartIndex -= lostDeviceCount;
667 }
668 }
669 }
670
671 /// <summary>
672 /// Unpair all devices from the user and remove the user.
673 /// </summary>
674 /// <remarks>
675 /// If actions are associated with the user (<see cref="actions"/>), the list of devices used by the
676 /// actions (<see cref="IInputActionCollection.devices"/>) is reset as is the binding mask (<see
677 /// cref="IInputActionCollection.bindingMask"/>) in case a control scheme is activated on the user.
678 ///
679 /// Sends <see cref="InputUserChange.DeviceUnpaired"/> through <see cref="onChange"/> for every device
680 /// unpaired from the user.
681 ///
682 /// Sends <see cref="InputUserChange.Removed"/>.
683 /// </remarks>
684 /// <seealso cref="PerformPairingWithDevice"/>
685 /// <seealso cref="pairedDevices"/>
686 /// <seealso cref="UnpairDevice"/>
687 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
688 /// <seealso cref="InputUserChange.DeviceUnpaired"/>
689 /// <seealso cref="InputUserChange.Removed"/>
690 public void UnpairDevicesAndRemoveUser()
691 {
692 UnpairDevices();
693
694 var userIndex = index;
695 RemoveUser(userIndex);
696
697 m_Id = default;
698 }
699
700 /// <summary>
701 /// Return a list of all currently added devices that are not paired to any user.
702 /// </summary>
703 /// <returns>A (possibly empty) list of devices that are currently not paired to a user.</returns>
704 /// <remarks>
705 /// The resulting list uses <see cref="Allocator.Temp"> temporary, unmanaged memory</see>. If not disposed of
706 /// explicitly, the list will automatically be deallocated at the end of the frame and will become unusable.
707 /// </remarks>
708 /// <seealso cref="InputSystem.devices"/>
709 /// <seealso cref="pairedDevices"/>
710 /// <seealso cref="PerformPairingWithDevice"/>
711 public static InputControlList<InputDevice> GetUnpairedInputDevices()
712 {
713 var list = new InputControlList<InputDevice>(Allocator.Temp);
714 GetUnpairedInputDevices(ref list);
715 return list;
716 }
717
718 /// <summary>
719 /// Add all currently added devices that are not paired to any user to <paramref name="list"/>.
720 /// </summary>
721 /// <param name="list">List to add the devices to. Devices will be added to the end.</param>
722 /// <returns>Number of devices added to <paramref name="list"/>.</returns>
723 /// <seealso cref="InputSystem.devices"/>
724 /// <seealso cref="pairedDevices"/>
725 /// <seealso cref="PerformPairingWithDevice"/>
726 public static int GetUnpairedInputDevices(ref InputControlList<InputDevice> list)
727 {
728 var countBefore = list.Count;
729 foreach (var device in InputSystem.devices)
730 {
731 // If it's in s_AllPairedDevices, there is *some* user that is using the device.
732 // We don't care which one it is here.
733 if (ArrayHelpers.ContainsReference(s_GlobalState.allPairedDevices, s_GlobalState.allPairedDeviceCount, device))
734 continue;
735
736 list.Add(device);
737 }
738
739 return list.Count - countBefore;
740 }
741
742 /// <summary>
743 /// Find the user (if any) that <paramref name="device"/> is currently paired to.
744 /// </summary>
745 /// <param name="device">An input device.</param>
746 /// <returns>The user that <paramref name="device"/> is currently paired to or <c>null</c> if the device
747 /// is not currently paired to an user.</returns>
748 /// <remarks>
749 /// Note that multiple users may be paired to the same device. If that is the case for <paramref name="device"/>,
750 /// the method will return one of the users with no guarantee which one it is.
751 ///
752 /// To find all users paired to a device requires manually going through the list of users and their paired
753 /// devices.
754 /// </remarks>
755 /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception>
756 /// <seealso cref="pairedDevices"/>
757 /// <seealso cref="PerformPairingWithDevice"/>
758 public static InputUser? FindUserPairedToDevice(InputDevice device)
759 {
760 if (device == null)
761 throw new ArgumentNullException(nameof(device));
762
763 var userIndex = TryFindUserIndex(device);
764 if (userIndex == -1)
765 return null;
766
767 return s_GlobalState.allUsers[userIndex];
768 }
769
770 public static InputUser? FindUserByAccount(InputUserAccountHandle platformUserAccountHandle)
771 {
772 if (platformUserAccountHandle == default(InputUserAccountHandle))
773 throw new ArgumentException("Empty platform user account handle", nameof(platformUserAccountHandle));
774
775 var userIndex = TryFindUserIndex(platformUserAccountHandle);
776 if (userIndex == -1)
777 return null;
778
779 return s_GlobalState.allUsers[userIndex];
780 }
781
782 public static InputUser CreateUserWithoutPairedDevices()
783 {
784 var userIndex = AddUser();
785 return s_GlobalState.allUsers[userIndex];
786 }
787
788 ////REVIEW: allow re-adding a user through this method?
789 /// <summary>
790 /// Pair the given device to a user.
791 /// </summary>
792 /// <param name="device">Device to pair to a user.</param>
793 /// <param name="user">Optional parameter. If given, instead of creating a new user to pair the device
794 /// to, the device is paired to the given user.</param>
795 /// <param name="options">Optional set of options to modify pairing behavior.</param>
796 /// <remarks>
797 /// By default, a new user is created and <paramref name="device"/> is added <see cref="pairedDevices"/>
798 /// of the user and <see cref="InputUserChange.DevicePaired"/> is sent on <see cref="onChange"/>.
799 ///
800 /// If a valid user is supplied to <paramref name="user"/>, the device is paired to the given user instead
801 /// of creating a new user. By default, the device is added to the list of already paired devices for the user.
802 /// This can be changed by using <see cref="InputUserPairingOptions.UnpairCurrentDevicesFromUser"/> which causes
803 /// devices currently paired to the user to first be unpaired.
804 ///
805 /// The method will not prevent pairing of the same device to multiple users.
806 ///
807 /// Note that if the user has an associated set of actions (<see cref="actions"/>), the list of devices on the
808 /// actions (<see cref="IInputActionCollection.devices"/>) will automatically be updated meaning that the newly
809 /// paired devices will automatically reflect in the set of devices available to the user's actions. If the
810 /// user has a control scheme that is currently activated (<see cref="controlScheme"/>), then <see cref="controlSchemeMatch"/>
811 /// will also automatically update to reflect the matching of devices to the control scheme's device requirements.
812 ///
813 /// <example>
814 /// <code>
815 /// // Pair device to new user.
816 /// var user = InputUser.PerformPairingWithDevice(wand1);
817 ///
818 /// // Pair another device to the same user.
819 /// InputUser.PerformPairingWithDevice(wand2, user: user);
820 /// </code>
821 /// </example>
822 /// </remarks>
823 /// <seealso cref="pairedDevices"/>
824 /// <seealso cref="UnpairDevice"/>
825 /// <seealso cref="UnpairDevices"/>
826 /// <seealso cref="UnpairDevicesAndRemoveUser"/>
827 /// <seealso cref="InputUserChange.DevicePaired"/>
828 public static InputUser PerformPairingWithDevice(InputDevice device,
829 InputUser user = default,
830 InputUserPairingOptions options = InputUserPairingOptions.None)
831 {
832 if (device == null)
833 throw new ArgumentNullException(nameof(device));
834 if (user != default && !user.valid)
835 throw new ArgumentException("Invalid user", nameof(user));
836
837 // Create new user, if needed.
838 int userIndex;
839 if (user == default)
840 {
841 userIndex = AddUser();
842 }
843 else
844 {
845 // We have an existing user.
846 userIndex = user.index;
847
848 // See if we're supposed to clear out the user's currently paired devices first.
849 if ((options & InputUserPairingOptions.UnpairCurrentDevicesFromUser) != 0)
850 user.UnpairDevices();
851
852 // Ignore call if device is already paired to user.
853 if (user.pairedDevices.ContainsReference(device))
854 {
855 // Still might have to initiate user account selection.
856 if ((options & InputUserPairingOptions.ForcePlatformUserAccountSelection) != 0)
857 InitiateUserAccountSelection(userIndex, device, options);
858 return user;
859 }
860 }
861
862 // Handle the user account side of pairing.
863 var accountSelectionInProgress = InitiateUserAccountSelection(userIndex, device, options);
864
865 // Except if we have initiate user account selection, pair the device to
866 // to the user now.
867 if (!accountSelectionInProgress)
868 AddDeviceToUser(userIndex, device);
869
870 return s_GlobalState.allUsers[userIndex];
871 }
872
873 private static bool InitiateUserAccountSelection(int userIndex, InputDevice device,
874 InputUserPairingOptions options)
875 {
876 // See if there's a platform user account we can get from the device.
877 // NOTE: We don't query the current user account if the caller has opted to force account selection.
878 var queryUserAccountResult =
879 (options & InputUserPairingOptions.ForcePlatformUserAccountSelection) == 0
880 ? UpdatePlatformUserAccount(userIndex, device)
881 : 0;
882
883 ////REVIEW: what should we do if there already is an account selection in progress? InvalidOperationException?
884 // If the device supports user account selection but we didn't get one,
885 // try to initiate account selection.
886 if ((options & InputUserPairingOptions.ForcePlatformUserAccountSelection) != 0 ||
887 (queryUserAccountResult != InputDeviceCommand.GenericFailure &&
888 (queryUserAccountResult & (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount) == 0 &&
889 (options & InputUserPairingOptions.ForceNoPlatformUserAccountSelection) == 0))
890 {
891 if (InitiateUserAccountSelectionAtPlatformLevel(device))
892 {
893 s_GlobalState.allUserData[userIndex].flags |= UserFlags.UserAccountSelectionInProgress;
894 s_GlobalState.ongoingAccountSelections.Append(
895 new OngoingAccountSelection
896 {
897 device = device,
898 userId = s_GlobalState.allUsers[userIndex].id,
899 });
900
901 // Make sure we receive a notification for the configuration event.
902 HookIntoDeviceChange();
903
904 // Tell listeners that we started an account selection.
905 Notify(userIndex, InputUserChange.AccountSelectionInProgress, device);
906
907 return true;
908 }
909 }
910
911 return false;
912 }
913
914 public bool Equals(InputUser other)
915 {
916 return m_Id == other.m_Id;
917 }
918
919 public override bool Equals(object obj)
920 {
921 if (ReferenceEquals(null, obj))
922 return false;
923 return obj is InputUser && Equals((InputUser)obj);
924 }
925
926 public override int GetHashCode()
927 {
928 return (int)m_Id;
929 }
930
931 public static bool operator==(InputUser left, InputUser right)
932 {
933 return left.m_Id == right.m_Id;
934 }
935
936 public static bool operator!=(InputUser left, InputUser right)
937 {
938 return left.m_Id != right.m_Id;
939 }
940
941 /// <summary>
942 /// Add a new user.
943 /// </summary>
944 /// <returns>Index of the newly created user.</returns>
945 /// <remarks>
946 /// Adding a user sends a notification with <see cref="InputUserChange.Added"/> through <see cref="onChange"/>.
947 ///
948 /// The user will start out with no devices and no actions assigned.
949 ///
950 /// The user is added to <see cref="all"/>.
951 /// </remarks>
952 private static int AddUser()
953 {
954 var id = ++s_GlobalState.lastUserId;
955
956 // Add to list.
957 var userCount = s_GlobalState.allUserCount;
958 ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allUsers, ref userCount, new InputUser { m_Id = id });
959 var userIndex = ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allUserData, ref s_GlobalState.allUserCount, new UserData());
960
961 // Send notification.
962 Notify(userIndex, InputUserChange.Added, null);
963
964 return userIndex;
965 }
966
967 /// <summary>
968 /// Remove an active user.
969 /// </summary>
970 /// <param name="userIndex">Index of active user.</param>
971 /// <remarks>
972 /// Removing a user also unassigns all currently assigned devices from the user. On completion of this
973 /// method, <see cref="pairedDevices"/> of <paramref name="user"/> will be empty.
974 /// </remarks>
975 private static void RemoveUser(int userIndex)
976 {
977 Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid");
978 Debug.Assert(s_GlobalState.allUserData[userIndex].deviceCount == 0, "User must not have paired devices still");
979
980 // Reset data from control scheme.
981 if (s_GlobalState.allUserData[userIndex].controlScheme != null)
982 {
983 if (s_GlobalState.allUserData[userIndex].actions != null)
984 s_GlobalState.allUserData[userIndex].actions.bindingMask = null;
985 }
986 s_GlobalState.allUserData[userIndex].controlSchemeMatch.Dispose();
987
988 // Remove lost devices.
989 RemoveLostDevicesForUser(userIndex);
990
991 // Remove account selections that are in progress.
992 for (var i = 0; i < s_GlobalState.ongoingAccountSelections.length; ++i)
993 {
994 if (s_GlobalState.ongoingAccountSelections[i].userId != s_GlobalState.allUsers[userIndex].id)
995 continue;
996
997 s_GlobalState.ongoingAccountSelections.RemoveAtByMovingTailWithCapacity(i);
998 --i;
999 }
1000
1001 // Send notification (do before we actually remove the user).
1002 Notify(userIndex, InputUserChange.Removed, null);
1003
1004 // Remove.
1005 var userCount = s_GlobalState.allUserCount;
1006 s_GlobalState.allUsers.EraseAtWithCapacity(ref userCount, userIndex);
1007 s_GlobalState.allUserData.EraseAtWithCapacity(ref s_GlobalState.allUserCount, userIndex);
1008
1009 // Remove our hook if we no longer need it.
1010 if (s_GlobalState.allUserCount == 0)
1011 {
1012 UnhookFromDeviceChange();
1013 UnhookFromActionChange();
1014 }
1015 }
1016
1017 private static void Notify(int userIndex, InputUserChange change, InputDevice device)
1018 {
1019 Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid");
1020
1021 if (s_GlobalState.onChange.length == 0)
1022 return;
1023 k_InputUserOnChangeMarker.Begin();
1024 s_GlobalState.onChange.LockForChanges();
1025 for (var i = 0; i < s_GlobalState.onChange.length; ++i)
1026 {
1027 try
1028 {
1029 s_GlobalState.onChange[i](s_GlobalState.allUsers[userIndex], change, device);
1030 }
1031 catch (Exception exception)
1032 {
1033 Debug.LogError($"{exception.GetType().Name} while executing 'InputUser.onChange' callbacks");
1034 Debug.LogException(exception);
1035 }
1036 }
1037 s_GlobalState.onChange.UnlockForChanges();
1038 k_InputUserOnChangeMarker.End();
1039 }
1040
1041 private static int TryFindUserIndex(uint userId)
1042 {
1043 Debug.Assert(userId != InvalidId, "User ID is invalid");
1044
1045 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1046 {
1047 if (s_GlobalState.allUsers[i].m_Id == userId)
1048 return i;
1049 }
1050 return -1;
1051 }
1052
1053 private static int TryFindUserIndex(InputUserAccountHandle platformHandle)
1054 {
1055 Debug.Assert(platformHandle != default, "User platform handle is invalid");
1056
1057 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1058 {
1059 if (s_GlobalState.allUserData[i].platformUserAccountHandle == platformHandle)
1060 return i;
1061 }
1062 return -1;
1063 }
1064
1065 /// <summary>
1066 /// Find the user (if any) that is currently assigned the given <paramref name="device"/>.
1067 /// </summary>
1068 /// <param name="device">An input device that has been added to the system.</param>
1069 /// <returns>Index of the user that has <paramref name="device"/> among its <see cref="pairedDevices"/> or -1 if
1070 /// no user is currently assigned the given device.</returns>
1071 private static int TryFindUserIndex(InputDevice device)
1072 {
1073 Debug.Assert(device != null, "Device cannot be null");
1074
1075 var indexOfDevice = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount);
1076 if (indexOfDevice == -1)
1077 return -1;
1078
1079 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1080 {
1081 var startIndex = s_GlobalState.allUserData[i].deviceStartIndex;
1082 if (startIndex <= indexOfDevice && indexOfDevice < startIndex + s_GlobalState.allUserData[i].deviceCount)
1083 return i;
1084 }
1085
1086 return -1;
1087 }
1088
1089 /// <summary>
1090 /// Add the given device to the user as either a lost device or a paired device.
1091 /// </summary>
1092 /// <param name="userIndex"></param>
1093 /// <param name="device"></param>
1094 /// <param name="asLostDevice"></param>
1095 private static void AddDeviceToUser(int userIndex, InputDevice device, bool asLostDevice = false, bool dontUpdateControlScheme = false)
1096 {
1097 Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid");
1098 Debug.Assert(device != null, "Device cannot be null");
1099 if (asLostDevice)
1100 Debug.Assert(!s_GlobalState.allUsers[userIndex].lostDevices.ContainsReference(device), "Device already in set of lostDevices for user");
1101 else
1102 Debug.Assert(!s_GlobalState.allUsers[userIndex].pairedDevices.ContainsReference(device), "Device already in set of pairedDevices for user");
1103
1104 var deviceCount = asLostDevice
1105 ? s_GlobalState.allUserData[userIndex].lostDeviceCount
1106 : s_GlobalState.allUserData[userIndex].deviceCount;
1107 var deviceStartIndex = asLostDevice
1108 ? s_GlobalState.allUserData[userIndex].lostDeviceStartIndex
1109 : s_GlobalState.allUserData[userIndex].deviceStartIndex;
1110
1111 ++s_GlobalState.pairingStateVersion;
1112
1113 // Move our devices to end of array.
1114 if (deviceCount > 0)
1115 {
1116 ArrayHelpers.MoveSlice(asLostDevice ? s_GlobalState.allLostDevices : s_GlobalState.allPairedDevices, deviceStartIndex,
1117 asLostDevice ? s_GlobalState.allLostDeviceCount - deviceCount : s_GlobalState.allPairedDeviceCount - deviceCount,
1118 deviceCount);
1119
1120 // Adjust users that have been impacted by the change.
1121 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1122 {
1123 if (i == userIndex)
1124 continue;
1125
1126 if ((asLostDevice ? s_GlobalState.allUserData[i].lostDeviceStartIndex : s_GlobalState.allUserData[i].deviceStartIndex) <= deviceStartIndex)
1127 continue;
1128
1129 if (asLostDevice)
1130 s_GlobalState.allUserData[i].lostDeviceStartIndex -= deviceCount;
1131 else
1132 s_GlobalState.allUserData[i].deviceStartIndex -= deviceCount;
1133 }
1134 }
1135
1136 // Append to array.
1137 if (asLostDevice)
1138 {
1139 s_GlobalState.allUserData[userIndex].lostDeviceStartIndex = s_GlobalState.allLostDeviceCount - deviceCount;
1140 ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allLostDevices, ref s_GlobalState.allLostDeviceCount, device);
1141 ++s_GlobalState.allUserData[userIndex].lostDeviceCount;
1142 }
1143 else
1144 {
1145 s_GlobalState.allUserData[userIndex].deviceStartIndex = s_GlobalState.allPairedDeviceCount - deviceCount;
1146 ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allPairedDevices, ref s_GlobalState.allPairedDeviceCount, device);
1147 ++s_GlobalState.allUserData[userIndex].deviceCount;
1148
1149 // If the user has actions, sync the devices on them with what we have now.
1150 var actions = s_GlobalState.allUserData[userIndex].actions;
1151 if (actions != null)
1152 {
1153 actions.devices = s_GlobalState.allUsers[userIndex].pairedDevices;
1154
1155 // Also, if we have a control scheme, update the matching of device requirements
1156 // against the device we now have.
1157 if (!dontUpdateControlScheme && s_GlobalState.allUserData[userIndex].controlScheme != null)
1158 UpdateControlSchemeMatch(userIndex);
1159 }
1160 }
1161
1162 // Make sure we get OnDeviceChange notifications.
1163 HookIntoDeviceChange();
1164
1165 // Let listeners know.
1166 Notify(userIndex, asLostDevice ? InputUserChange.DeviceLost : InputUserChange.DevicePaired, device);
1167 }
1168
1169 private static void RemoveDeviceFromUser(int userIndex, InputDevice device, bool asLostDevice = false)
1170 {
1171 Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid");
1172 Debug.Assert(device != null, "Device cannot be null");
1173
1174 var deviceIndex = asLostDevice
1175 ? s_GlobalState.allLostDevices.IndexOfReference(device, s_GlobalState.allLostDeviceCount)
1176 : s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allUserData[userIndex].deviceStartIndex,
1177 s_GlobalState.allUserData[userIndex].deviceCount);
1178 if (deviceIndex == -1)
1179 {
1180 // Device not in list. Ignore.
1181 return;
1182 }
1183
1184 if (asLostDevice)
1185 {
1186 s_GlobalState.allLostDevices.EraseAtWithCapacity(ref s_GlobalState.allLostDeviceCount, deviceIndex);
1187 --s_GlobalState.allUserData[userIndex].lostDeviceCount;
1188 }
1189 else
1190 {
1191 ++s_GlobalState.pairingStateVersion;
1192 s_GlobalState.allPairedDevices.EraseAtWithCapacity(ref s_GlobalState.allPairedDeviceCount, deviceIndex);
1193 --s_GlobalState.allUserData[userIndex].deviceCount;
1194 }
1195
1196 // Adjust indices of other users.
1197 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1198 {
1199 if ((asLostDevice ? s_GlobalState.allUserData[i].lostDeviceStartIndex : s_GlobalState.allUserData[i].deviceStartIndex) <= deviceIndex)
1200 continue;
1201
1202 if (asLostDevice)
1203 --s_GlobalState.allUserData[i].lostDeviceStartIndex;
1204 else
1205 --s_GlobalState.allUserData[i].deviceStartIndex;
1206 }
1207
1208 if (!asLostDevice)
1209 {
1210 // Remove any ongoing account selections for the user on the given device.
1211 for (var i = 0; i < s_GlobalState.ongoingAccountSelections.length; ++i)
1212 {
1213 if (s_GlobalState.ongoingAccountSelections[i].userId != s_GlobalState.allUsers[userIndex].id ||
1214 s_GlobalState.ongoingAccountSelections[i].device != device)
1215 continue;
1216
1217 s_GlobalState.ongoingAccountSelections.RemoveAtByMovingTailWithCapacity(i);
1218 --i;
1219 }
1220
1221 // If the user has actions, sync the devices on them with what we have now.
1222 var actions = s_GlobalState.allUserData[userIndex].actions;
1223 if (actions != null)
1224 {
1225 actions.devices = s_GlobalState.allUsers[userIndex].pairedDevices;
1226
1227 if (s_GlobalState.allUsers[userIndex].controlScheme != null)
1228 UpdateControlSchemeMatch(userIndex);
1229 }
1230
1231 // Notify listeners.
1232 Notify(userIndex, InputUserChange.DeviceUnpaired, device);
1233 }
1234 }
1235
1236 private static void UpdateControlSchemeMatch(int userIndex, bool autoPairMissing = false)
1237 {
1238 Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid");
1239
1240 // Nothing to do if we don't have a control scheme.
1241 if (s_GlobalState.allUserData[userIndex].controlScheme == null)
1242 return;
1243
1244 // Get rid of last match result and start new match.
1245 s_GlobalState.allUserData[userIndex].controlSchemeMatch.Dispose();
1246 var matchResult = new InputControlScheme.MatchResult();
1247 try
1248 {
1249 // Match the control scheme's requirements against the devices paired to the user.
1250 var scheme = s_GlobalState.allUserData[userIndex].controlScheme.Value;
1251 if (scheme.deviceRequirements.Count > 0)
1252 {
1253 var availableDevices = new InputControlList<InputDevice>(Allocator.Temp);
1254 try
1255 {
1256 // Add devices already paired to user.
1257 availableDevices.AddSlice(s_GlobalState.allUsers[userIndex].pairedDevices);
1258
1259 // If we're supposed to grab whatever additional devices we need from what's
1260 // available, add all unpaired devices to the list.
1261 // NOTE: These devices go *after* the devices already paired (if any) meaning that
1262 // the control scheme matching will grab already paired devices *first*.
1263 if (autoPairMissing)
1264 {
1265 var startIndex = availableDevices.Count;
1266 var count = GetUnpairedInputDevices(ref availableDevices);
1267
1268 // We want to favor devices that are already assigned to the same platform user account.
1269 // Sort the unpaired devices we've added to the list such that the ones belonging to the
1270 // same user account come first.
1271 if (s_GlobalState.allUserData[userIndex].platformUserAccountHandle != null)
1272 availableDevices.Sort(startIndex, count,
1273 new CompareDevicesByUserAccount
1274 {
1275 platformUserAccountHandle = s_GlobalState.allUserData[userIndex].platformUserAccountHandle.Value
1276 });
1277 }
1278
1279 matchResult = scheme.PickDevicesFrom(availableDevices);
1280 if (matchResult.isSuccessfulMatch)
1281 {
1282 // Control scheme is satisfied with the devices we have available.
1283 // If we may have grabbed as of yet unpaired devices, go and pair them to the user.
1284 if (autoPairMissing)
1285 {
1286 // Update match result on user before potentially invoking callbacks.
1287 s_GlobalState.allUserData[userIndex].controlSchemeMatch = matchResult;
1288
1289 foreach (var device in matchResult.devices)
1290 {
1291 // Skip if already paired to user.
1292 if (s_GlobalState.allUsers[userIndex].pairedDevices.ContainsReference(device))
1293 continue;
1294
1295 AddDeviceToUser(userIndex, device, dontUpdateControlScheme: true);
1296 }
1297 }
1298 }
1299 }
1300 finally
1301 {
1302 availableDevices.Dispose();
1303 }
1304 }
1305
1306 s_GlobalState.allUserData[userIndex].controlSchemeMatch = matchResult;
1307 }
1308 catch (Exception)
1309 {
1310 // If we had an exception and are bailing out, make sure we aren't leaking native memory
1311 // we allocated.
1312 matchResult.Dispose();
1313 throw;
1314 }
1315 }
1316
1317 private static long UpdatePlatformUserAccount(int userIndex, InputDevice device)
1318 {
1319 Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid");
1320
1321 // Fetch account details from backend.
1322 var queryResult = QueryPairedPlatformUserAccount(device, out var platformUserAccountHandle,
1323 out var platformUserAccountName, out var platformUserAccountId);
1324
1325 // Nothing much to do if not supported by device.
1326 if (queryResult == InputDeviceCommand.GenericFailure)
1327 {
1328 // Check if there's an account selection in progress. There shouldn't be as it's
1329 // weird for the device to no signal it does not support querying user account, but
1330 // just to be safe, we check.
1331 if ((s_GlobalState.allUserData[userIndex].flags & UserFlags.UserAccountSelectionInProgress) != 0)
1332 Notify(userIndex, InputUserChange.AccountSelectionCanceled, null);
1333
1334 s_GlobalState.allUserData[userIndex].platformUserAccountHandle = null;
1335 s_GlobalState.allUserData[userIndex].platformUserAccountName = null;
1336 s_GlobalState.allUserData[userIndex].platformUserAccountId = null;
1337
1338 return queryResult;
1339 }
1340
1341 // Check if there's an account selection that we have initiated.
1342 if ((s_GlobalState.allUserData[userIndex].flags & UserFlags.UserAccountSelectionInProgress) != 0)
1343 {
1344 // Yes, there is. See if it is complete.
1345
1346 if ((queryResult & (long)QueryPairedUserAccountCommand.Result.UserAccountSelectionInProgress) != 0)
1347 {
1348 // No, still in progress.
1349 }
1350 else if ((queryResult & (long)QueryPairedUserAccountCommand.Result.UserAccountSelectionCanceled) != 0)
1351 {
1352 // Got canceled.
1353 Notify(userIndex, InputUserChange.AccountSelectionCanceled, device);
1354 }
1355 else
1356 {
1357 // Yes, it is complete.
1358 s_GlobalState.allUserData[userIndex].flags &= ~UserFlags.UserAccountSelectionInProgress;
1359
1360 s_GlobalState.allUserData[userIndex].platformUserAccountHandle = platformUserAccountHandle;
1361 s_GlobalState.allUserData[userIndex].platformUserAccountName = platformUserAccountName;
1362 s_GlobalState.allUserData[userIndex].platformUserAccountId = platformUserAccountId;
1363
1364 Notify(userIndex, InputUserChange.AccountSelectionComplete, device);
1365 }
1366 }
1367 // Check if user account details have changed.
1368 else if (s_GlobalState.allUserData[userIndex].platformUserAccountHandle != platformUserAccountHandle ||
1369 s_GlobalState.allUserData[userIndex].platformUserAccountId != platformUserAccountId)
1370 {
1371 s_GlobalState.allUserData[userIndex].platformUserAccountHandle = platformUserAccountHandle;
1372 s_GlobalState.allUserData[userIndex].platformUserAccountName = platformUserAccountName;
1373 s_GlobalState.allUserData[userIndex].platformUserAccountId = platformUserAccountId;
1374
1375 Notify(userIndex, InputUserChange.AccountChanged, device);
1376 }
1377 else if (s_GlobalState.allUserData[userIndex].platformUserAccountName != platformUserAccountName)
1378 {
1379 Notify(userIndex, InputUserChange.AccountNameChanged, device);
1380 }
1381
1382 return queryResult;
1383 }
1384
1385 ////TODO: bring documentation for these back when user management is implemented on Xbox and PS
1386 private static long QueryPairedPlatformUserAccount(InputDevice device,
1387 out InputUserAccountHandle? platformAccountHandle, out string platformAccountName, out string platformAccountId)
1388 {
1389 Debug.Assert(device != null, "Device cannot be null");
1390
1391 // Query user account info from backend.
1392 var queryPairedUser = QueryPairedUserAccountCommand.Create();
1393 var result = device.ExecuteCommand(ref queryPairedUser);
1394 if (result == InputDeviceCommand.GenericFailure)
1395 {
1396 // Not currently paired to user account in backend.
1397 platformAccountHandle = null;
1398 platformAccountName = null;
1399 platformAccountId = null;
1400 return InputDeviceCommand.GenericFailure;
1401 }
1402
1403 // Success. There is a user account currently paired to the device and we now have the
1404 // platform's user account details.
1405
1406 if ((result & (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount) != 0)
1407 {
1408 platformAccountHandle =
1409 new InputUserAccountHandle(device.description.interfaceName ?? "<Unknown>", queryPairedUser.handle);
1410 platformAccountName = queryPairedUser.name;
1411 platformAccountId = queryPairedUser.id;
1412 }
1413 else
1414 {
1415 // The device supports QueryPairedUserAccountCommand but reports that the
1416 // device is not currently paired to a user.
1417 //
1418 // NOTE: On Switch, where the system itself does not store account<->pairing, we will always
1419 // end up here until we've initiated an account selection through the backend itself.
1420 platformAccountHandle = null;
1421 platformAccountName = null;
1422 platformAccountId = null;
1423 }
1424
1425 return result;
1426 }
1427
1428 /// <summary>
1429 /// Try to initiate user account pairing for the given device at the platform level.
1430 /// </summary>
1431 /// <param name="device"></param>
1432 /// <returns>True if the device accepted the request and an account picker has been raised.</returns>
1433 /// <remarks>
1434 /// Sends <see cref="InitiateUserAccountPairingCommand"/> to the device.
1435 /// </remarks>
1436 private static bool InitiateUserAccountSelectionAtPlatformLevel(InputDevice device)
1437 {
1438 Debug.Assert(device != null, "Device cannot be null");
1439
1440 var initiateUserPairing = InitiateUserAccountPairingCommand.Create();
1441 var initiatePairingResult = device.ExecuteCommand(ref initiateUserPairing);
1442 if (initiatePairingResult == (long)InitiateUserAccountPairingCommand.Result.ErrorAlreadyInProgress)
1443 throw new InvalidOperationException("User pairing already in progress");
1444
1445 return initiatePairingResult == (long)InitiateUserAccountPairingCommand.Result.SuccessfullyInitiated;
1446 }
1447
1448 private static void OnActionChange(object obj, InputActionChange change)
1449 {
1450 if (change == InputActionChange.BoundControlsChanged)
1451 {
1452 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1453 {
1454 ref var user = ref s_GlobalState.allUsers[i];
1455 if (ReferenceEquals(user.actions, obj))
1456 Notify(i, InputUserChange.ControlsChanged, null);
1457 }
1458 }
1459 }
1460
1461 /// <summary>
1462 /// Invoked in response to <see cref="InputSystem.onDeviceChange"/>.
1463 /// </summary>
1464 /// <param name="device"></param>
1465 /// <param name="change"></param>
1466 /// <remarks>
1467 /// We monitor the device setup in the system for activity that impacts the user setup.
1468 /// </remarks>
1469 private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
1470 {
1471 switch (change)
1472 {
1473 // Existing device removed. May mean a user has lost a device due to the battery running
1474 // out or the device being unplugged.
1475 // NOTE: We ignore Disconnected here. Removed is what gets sent whenever a device is taken off of
1476 // InputSystem.devices -- which is what we're interested in here.
1477 case InputDeviceChange.Removed:
1478 {
1479 // Could have been removed from multiple users. Repeatedly search in s_AllPairedDevices
1480 // until we can't find the device anymore.
1481 var deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount);
1482 while (deviceIndex != -1)
1483 {
1484 // Find user. Must be there as we found the device in s_AllPairedDevices.
1485 var userIndex = -1;
1486 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1487 {
1488 var deviceStartIndex = s_GlobalState.allUserData[i].deviceStartIndex;
1489 if (deviceStartIndex <= deviceIndex && deviceIndex < deviceStartIndex + s_GlobalState.allUserData[i].deviceCount)
1490 {
1491 userIndex = i;
1492 break;
1493 }
1494 }
1495
1496 // Add device to list of lost devices.
1497 // NOTE: This will also send a DeviceLost notification.
1498 // NOTE: Temporarily the device is on both lists.
1499 AddDeviceToUser(userIndex, device, asLostDevice: true);
1500
1501 // Remove it from the user.
1502 RemoveDeviceFromUser(userIndex, device);
1503
1504 // Search for another user paired to the same device.
1505 deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount);
1506 }
1507 break;
1508 }
1509
1510 // New device was added. See if it was a device we previously lost on a user.
1511 case InputDeviceChange.Added:
1512 {
1513 // Search all lost devices. Could affect multiple users.
1514 // Note that RemoveDeviceFromUser removes one element, hence no advancement of deviceIndex.
1515 for (var deviceIndex = FindLostDevice(device); deviceIndex != -1;
1516 deviceIndex = FindLostDevice(device, deviceIndex))
1517 {
1518 // Find user. Must be there as we found the device in s_AllLostDevices.
1519 var userIndex = -1;
1520 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1521 {
1522 var deviceStartIndex = s_GlobalState.allUserData[i].lostDeviceStartIndex;
1523 if (deviceStartIndex <= deviceIndex && deviceIndex < deviceStartIndex + s_GlobalState.allUserData[i].lostDeviceCount)
1524 {
1525 userIndex = i;
1526 break;
1527 }
1528 }
1529
1530 // Remove from list of lost devices. No notification. Notice that we need to use device
1531 // from lost device list even if its another instance.
1532 RemoveDeviceFromUser(userIndex, s_GlobalState.allLostDevices[deviceIndex], asLostDevice: true);
1533
1534 // Notify.
1535 Notify(userIndex, InputUserChange.DeviceRegained, device);
1536
1537 // Add back as normally paired device.
1538 AddDeviceToUser(userIndex, device);
1539 }
1540 break;
1541 }
1542
1543 // Device had its configuration changed which may mean we have a different user account paired
1544 // to the device now.
1545 case InputDeviceChange.ConfigurationChanged:
1546 {
1547 // See if the this is a device that we were waiting for an account selection on. If so, pair
1548 // it to the user that was waiting.
1549 var wasOngoingAccountSelection = false;
1550 for (var i = 0; i < s_GlobalState.ongoingAccountSelections.length; ++i)
1551 {
1552 if (s_GlobalState.ongoingAccountSelections[i].device != device)
1553 continue;
1554
1555 var userIndex = new InputUser { m_Id = s_GlobalState.ongoingAccountSelections[i].userId }.index;
1556 var queryResult = UpdatePlatformUserAccount(userIndex, device);
1557 if ((queryResult & (long)QueryPairedUserAccountCommand.Result.UserAccountSelectionInProgress) == 0)
1558 {
1559 wasOngoingAccountSelection = true;
1560 s_GlobalState.ongoingAccountSelections.RemoveAtByMovingTailWithCapacity(i);
1561 --i;
1562
1563 // If the device wasn't paired to the user, pair it now.
1564 if (!s_GlobalState.allUsers[userIndex].pairedDevices.ContainsReference(device))
1565 AddDeviceToUser(userIndex, device);
1566 }
1567 }
1568
1569 // If it wasn't a configuration change event from an account selection, go and check whether
1570 // there was a user account change that happened outside the application.
1571 if (!wasOngoingAccountSelection)
1572 {
1573 // Could be paired to multiple users. Repeatedly search in s_AllPairedDevices
1574 // until we can't find the device anymore.
1575 var deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount);
1576 while (deviceIndex != -1)
1577 {
1578 // Find user. Must be there as we found the device in s_AllPairedDevices.
1579 var userIndex = -1;
1580 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1581 {
1582 var deviceStartIndex = s_GlobalState.allUserData[i].deviceStartIndex;
1583 if (deviceStartIndex <= deviceIndex && deviceIndex < deviceStartIndex + s_GlobalState.allUserData[i].deviceCount)
1584 {
1585 userIndex = i;
1586 break;
1587 }
1588 }
1589
1590 // Check user account.
1591 UpdatePlatformUserAccount(userIndex, device);
1592
1593 // Search for another user paired to the same device.
1594 // Note that action is tied to user and hence we can skip to end of slice associated
1595 // with the current user or at least one element forward.
1596 var offsetNextSlice = deviceIndex + Math.Max(1, s_GlobalState.allUserData[userIndex].deviceCount);
1597 deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, offsetNextSlice, s_GlobalState.allPairedDeviceCount - offsetNextSlice);
1598 }
1599 }
1600 break;
1601 }
1602 }
1603 }
1604
1605 private static int FindLostDevice(InputDevice device, int startIndex = 0)
1606 {
1607 // Compare both by device ID and by reference. We may be looking at a device that was recreated
1608 // due to layout changes (new InputDevice instance, same ID) or a device that was reconnected
1609 // and thus fetched out of `disconnectedDevices` (same InputDevice instance, new ID).
1610
1611 var newDeviceId = device.deviceId;
1612 for (var i = startIndex; i < s_GlobalState.allLostDeviceCount; ++i)
1613 {
1614 var lostDevice = s_GlobalState.allLostDevices[i];
1615 if (device == lostDevice || lostDevice.deviceId == newDeviceId) return i;
1616 }
1617
1618 return -1;
1619 }
1620
1621 // We hook this into InputSystem.onEvent when listening for activity on unpaired devices.
1622 // What this means is that we get to run *before* state reaches the device. This in turn
1623 // means that should the device get paired as a result, actions that are enabled as part
1624 // of the pairing will immediately get triggered. This would not be the case if we hook
1625 // into InputState.onDeviceChange instead which only triggers once state has been altered.
1626 //
1627 // NOTE: This also means that unpaired device activity will *only* be detected from events,
1628 // NOT from state changes applied directly through InputState.Change.
1629 private static void OnEvent(InputEventPtr eventPtr, InputDevice device)
1630 {
1631 Debug.Assert(s_GlobalState.listenForUnpairedDeviceActivity != 0,
1632 "This should only be called while listening for unpaired device activity");
1633 if (s_GlobalState.listenForUnpairedDeviceActivity == 0)
1634 return;
1635
1636 // Ignore input in editor.
1637#if UNITY_EDITOR
1638 if (InputState.currentUpdateType == InputUpdateType.Editor)
1639 return;
1640#endif
1641
1642 // Ignore any state change not triggered from a state event.
1643 var eventType = eventPtr.type;
1644 if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type)
1645 return;
1646
1647 // Ignore event if device is disabled.
1648 if (!device.enabled)
1649 return;
1650
1651 // See if it's a device not belonging to any user.
1652 if (ArrayHelpers.ContainsReference(s_GlobalState.allPairedDevices, s_GlobalState.allPairedDeviceCount, device))
1653 {
1654 // No, it's a device already paired to a player so do nothing.
1655 return;
1656 }
1657
1658 k_InputCheckForUnpairMarker.Begin();
1659
1660 // Apply the pre-filter. If there's callbacks and none of them return true,
1661 // we early out and ignore the event entirely.
1662 if (!DelegateHelpers.InvokeCallbacksSafe_AnyCallbackReturnsTrue(
1663 ref s_GlobalState.onPreFilterUnpairedDeviceUsed, device, eventPtr, "InputUser.onPreFilterUnpairedDeviceActivity"))
1664 {
1665 k_InputCheckForUnpairMarker.End();
1666 return;
1667 }
1668
1669 // Go through the changed controls in the event and look for ones actuated
1670 // above a magnitude of a little above zero.
1671 foreach (var control in eventPtr.EnumerateChangedControls(device: device, magnitudeThreshold: 0.0001f))
1672 {
1673 var deviceHasBeenPaired = false;
1674 s_GlobalState.onUnpairedDeviceUsed.LockForChanges();
1675 for (var n = 0; n < s_GlobalState.onUnpairedDeviceUsed.length; ++n)
1676 {
1677 var pairingStateVersionBefore = s_GlobalState.pairingStateVersion;
1678
1679 try
1680 {
1681 s_GlobalState.onUnpairedDeviceUsed[n](control, eventPtr);
1682 }
1683 catch (Exception exception)
1684 {
1685 Debug.LogError($"{exception.GetType().Name} while executing 'InputUser.onUnpairedDeviceUsed' callbacks");
1686 Debug.LogException(exception);
1687 }
1688
1689 if (pairingStateVersionBefore != s_GlobalState.pairingStateVersion
1690 && FindUserPairedToDevice(device) != null)
1691 {
1692 deviceHasBeenPaired = true;
1693 break;
1694 }
1695 }
1696 s_GlobalState.onUnpairedDeviceUsed.UnlockForChanges();
1697
1698 // If the device was paired in one of the callbacks, stop processing
1699 // changes on it.
1700 if (deviceHasBeenPaired)
1701 break;
1702 }
1703
1704 k_InputCheckForUnpairMarker.End();
1705 }
1706
1707 /// <summary>
1708 /// Syntax for configuring a control scheme on a user.
1709 /// </summary>
1710 public struct ControlSchemeChangeSyntax
1711 {
1712 /// <summary>
1713 /// Leave the user's paired devices in place but pair any available devices
1714 /// that are still required by the control scheme.
1715 /// </summary>
1716 /// <returns></returns>
1717 /// <remarks>
1718 /// If there are unpaired devices that, at the platform level, are associated with the same
1719 /// user account, those will take precedence over other unpaired devices.
1720 /// </remarks>
1721 public ControlSchemeChangeSyntax AndPairRemainingDevices()
1722 {
1723 UpdateControlSchemeMatch(m_UserIndex, autoPairMissing: true);
1724 return this;
1725 }
1726
1727 internal int m_UserIndex;
1728 }
1729
1730 private uint m_Id;
1731
1732 [Flags]
1733 internal enum UserFlags
1734 {
1735 BindToAllDevices = 1 << 0,
1736
1737 /// <summary>
1738 /// Whether we have initiated a user account selection.
1739 /// </summary>
1740 UserAccountSelectionInProgress = 1 << 1,
1741 }
1742
1743 /// <summary>
1744 /// Data we store for each user.
1745 /// </summary>
1746 private struct UserData
1747 {
1748 /// <summary>
1749 /// The platform handle associated with the user.
1750 /// </summary>
1751 /// <remarks>
1752 /// If set, this identifies the user on the platform. It also means that the devices
1753 /// assigned to the user may be paired at the platform level.
1754 /// </remarks>
1755 public InputUserAccountHandle? platformUserAccountHandle;
1756
1757 /// <summary>
1758 /// Plain-text user name as returned by the underlying platform. Null if not associated with user on platform.
1759 /// </summary>
1760 public string platformUserAccountName;
1761
1762 /// <summary>
1763 /// Platform-specific ID that identifies the user across sessions even if the user
1764 /// name changes.
1765 /// </summary>
1766 /// <remarks>
1767 /// This might not be a human-readable string.
1768 /// </remarks>
1769 public string platformUserAccountId;
1770
1771 /// <summary>
1772 /// Number of devices in <see cref="InputUser.s_AllPairedDevices"/> assigned to the user.
1773 /// </summary>
1774 public int deviceCount;
1775
1776 /// <summary>
1777 /// Index in <see cref="InputUser.s_AllPairedDevices"/> where the devices for this user start. Only valid
1778 /// if <see cref="deviceCount"/> is greater than zero.
1779 /// </summary>
1780 public int deviceStartIndex;
1781
1782 /// <summary>
1783 /// Input actions associated with the user.
1784 /// </summary>
1785 public IInputActionCollection actions;
1786
1787 /// <summary>
1788 /// Currently active control scheme or null if no control scheme has been set on the user.
1789 /// </summary>
1790 /// <remarks>
1791 /// This also dictates the binding mask that we're using with <see cref="actions"/>.
1792 /// </remarks>
1793 public InputControlScheme? controlScheme;
1794
1795 public InputControlScheme.MatchResult controlSchemeMatch;
1796
1797 /// <summary>
1798 /// Number of devices in <see cref="InputUser.s_AllLostDevices"/> assigned to the user.
1799 /// </summary>
1800 public int lostDeviceCount;
1801
1802 /// <summary>
1803 /// Index in <see cref="InputUser.s_AllLostDevices"/> where the lost devices for this user start. Only valid
1804 /// if <see cref="lostDeviceCount"/> is greater than zero.
1805 /// </summary>
1806 public int lostDeviceStartIndex;
1807
1808 ////TODO
1809 //public InputUserSettings settings;
1810
1811 public UserFlags flags;
1812 }
1813
1814 /// <summary>
1815 /// Compare two devices for being associated with a specific platform user account.
1816 /// </summary>
1817 private struct CompareDevicesByUserAccount : IComparer<InputDevice>
1818 {
1819 public InputUserAccountHandle platformUserAccountHandle;
1820
1821 public int Compare(InputDevice x, InputDevice y)
1822 {
1823 var firstAccountHandle = GetUserAccountHandleForDevice(x);
1824 var secondAccountHandle = GetUserAccountHandleForDevice(x);
1825
1826 if (firstAccountHandle == platformUserAccountHandle &&
1827 secondAccountHandle == platformUserAccountHandle)
1828 return 0;
1829
1830 if (firstAccountHandle == platformUserAccountHandle)
1831 return -1;
1832
1833 if (secondAccountHandle == platformUserAccountHandle)
1834 return 1;
1835
1836 return 0;
1837 }
1838
1839 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "device", Justification = "Keep this for future implementation")]
1840 private static InputUserAccountHandle? GetUserAccountHandleForDevice(InputDevice device)
1841 {
1842 ////TODO (need to cache this)
1843 return null;
1844 }
1845 }
1846
1847 private struct OngoingAccountSelection
1848 {
1849 public InputDevice device;
1850 public uint userId;
1851 }
1852
1853 private struct GlobalState
1854 {
1855 internal int pairingStateVersion;
1856 internal uint lastUserId;
1857 internal int allUserCount;
1858 internal int allPairedDeviceCount;
1859 internal int allLostDeviceCount;
1860 internal InputUser[] allUsers;
1861 internal UserData[] allUserData;
1862 internal InputDevice[] allPairedDevices; // We keep a single array that we slice out to each user.
1863 internal InputDevice[] allLostDevices; // We keep a single array that we slice out to each user.
1864 internal InlinedArray<OngoingAccountSelection> ongoingAccountSelections;
1865 internal CallbackArray<Action<InputUser, InputUserChange, InputDevice>> onChange;
1866 internal CallbackArray<Action<InputControl, InputEventPtr>> onUnpairedDeviceUsed;
1867 internal CallbackArray<Func<InputDevice, InputEventPtr, bool>> onPreFilterUnpairedDeviceUsed;
1868 internal Action<object, InputActionChange> actionChangeDelegate;
1869 internal Action<InputDevice, InputDeviceChange> onDeviceChangeDelegate;
1870 internal Action<InputEventPtr, InputDevice> onEventDelegate;
1871 internal bool onActionChangeHooked;
1872 internal bool onDeviceChangeHooked;
1873 internal bool onEventHooked;
1874 internal int listenForUnpairedDeviceActivity;
1875 }
1876
1877 private static GlobalState s_GlobalState;
1878
1879 internal static ISavedState SaveAndResetState()
1880 {
1881 // Save current state and provide an opaque interface to restore it
1882 var savedState = new SavedStructState<GlobalState>(
1883 ref s_GlobalState,
1884 (ref GlobalState state) => s_GlobalState = state, // restore
1885 () => DisposeAndResetGlobalState()); // static dispose
1886
1887 // Reset global state
1888 s_GlobalState = default;
1889
1890 return savedState;
1891 }
1892
1893 private static void HookIntoActionChange()
1894 {
1895 if (s_GlobalState.onActionChangeHooked)
1896 return;
1897 if (s_GlobalState.actionChangeDelegate == null)
1898 s_GlobalState.actionChangeDelegate = OnActionChange;
1899 InputSystem.onActionChange += OnActionChange;
1900 s_GlobalState.onActionChangeHooked = true;
1901 }
1902
1903 private static void UnhookFromActionChange()
1904 {
1905 if (!s_GlobalState.onActionChangeHooked)
1906 return;
1907 InputSystem.onActionChange -= OnActionChange;
1908 s_GlobalState.onActionChangeHooked = false;
1909 }
1910
1911 private static void HookIntoDeviceChange()
1912 {
1913 if (s_GlobalState.onDeviceChangeHooked)
1914 return;
1915 if (s_GlobalState.onDeviceChangeDelegate == null)
1916 s_GlobalState.onDeviceChangeDelegate = OnDeviceChange;
1917 InputSystem.onDeviceChange += s_GlobalState.onDeviceChangeDelegate;
1918 s_GlobalState.onDeviceChangeHooked = true;
1919 }
1920
1921 private static void UnhookFromDeviceChange()
1922 {
1923 if (!s_GlobalState.onDeviceChangeHooked)
1924 return;
1925 InputSystem.onDeviceChange -= s_GlobalState.onDeviceChangeDelegate;
1926 s_GlobalState.onDeviceChangeHooked = false;
1927 }
1928
1929 private static void HookIntoEvents()
1930 {
1931 if (s_GlobalState.onEventHooked)
1932 return;
1933 if (s_GlobalState.onEventDelegate == null)
1934 s_GlobalState.onEventDelegate = OnEvent;
1935 InputSystem.onEvent += s_GlobalState.onEventDelegate;
1936 s_GlobalState.onEventHooked = true;
1937 }
1938
1939 private static void UnhookFromDeviceStateChange()
1940 {
1941 if (!s_GlobalState.onEventHooked)
1942 return;
1943 InputSystem.onEvent -= s_GlobalState.onEventDelegate;
1944 s_GlobalState.onEventHooked = false;
1945 }
1946
1947 private static void DisposeAndResetGlobalState()
1948 {
1949 // Release native memory held by control scheme match results.
1950 for (var i = 0; i < s_GlobalState.allUserCount; ++i)
1951 s_GlobalState.allUserData[i].controlSchemeMatch.Dispose();
1952
1953 // Don't reset s_LastUserId and just let it increment instead so we never generate
1954 // the same ID twice.
1955
1956 var storedLastUserId = s_GlobalState.lastUserId;
1957 s_GlobalState = default;
1958 s_GlobalState.lastUserId = storedLastUserId;
1959 }
1960
1961 internal static void ResetGlobals()
1962 {
1963 UnhookFromActionChange();
1964 UnhookFromDeviceChange();
1965 UnhookFromDeviceStateChange();
1966
1967 DisposeAndResetGlobalState();
1968 }
1969 }
1970}