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&amp;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: "&lt;Gamepad&gt;/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}