Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Plugins /Users /InputUser.cs
| using System; | |
| using System.Collections.Generic; | |
| using Unity.Collections; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEngine.InputSystem.Utilities; | |
| using UnityEngine.Profiling; | |
| ////REVIEW: remove users automatically when exiting play mode? | |
| ////REVIEW: do we need to handle the case where devices are added to a user that are each associated with a different user account | |
| ////REVIEW: how should we handle pairings of devices *not* called for by a control scheme? should that result in a failed match? | |
| ////TODO: option to bind to *all* devices instead of just the paired ones (bindToAllDevices) | |
| ////TODO: the account selection stuff needs cleanup; the current flow is too convoluted | |
| namespace UnityEngine.InputSystem.Users | |
| { | |
| /// <summary> | |
| /// Represents a specific user/player interacting with one or more devices and input actions. | |
| /// </summary> | |
| /// <remarks> | |
| /// Principally, an InputUser represents a human interacting with the application. Moreover, at any point | |
| /// each InputUser represents a human actor distinct from all other InputUsers in the system. | |
| /// | |
| /// Each user has one or more paired devices. In general, these devices are unique to each user. However, | |
| /// it is permitted to use <see cref="PerformPairingWithDevice"/> to pair the same device to multiple users. | |
| /// This can be useful in setups such as split-keyboard (e.g. one user using left side of keyboard and the | |
| /// other the right one) use or hotseat-style gameplay (e.g. two players taking turns on the same game controller). | |
| /// | |
| /// Note that the InputUser API, like <see cref="InputAction"/>) is a play mode-only feature. When exiting play mode, | |
| /// all users are automatically removed and all devices automatically unpaired. | |
| /// </remarks> | |
| /// <seealso cref="InputUserChange"/> | |
| public struct InputUser : IEquatable<InputUser> | |
| { | |
| public const uint InvalidId = 0; | |
| /// <summary> | |
| /// Whether this is a currently active user record in <see cref="all"/>. | |
| /// </summary> | |
| /// <remarks> | |
| /// Users that are removed (<see cref="UnpairDevicesAndRemoveUser"/>) will become invalid. | |
| /// </remarks> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| /// <seealso cref="InputUserChange.Removed"/> | |
| public bool valid | |
| { | |
| get | |
| { | |
| if (m_Id == InvalidId) | |
| return false; | |
| // See if there's a currently active user with the given ID. | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| if (s_GlobalState.allUsers[i].m_Id == m_Id) | |
| return true; | |
| return false; | |
| } | |
| } | |
| /// <summary> | |
| /// The sequence number of the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// It can be useful to establish a sorting of players locally such that it is | |
| /// known who is the first player, who is the second, and so on. This property | |
| /// gives the positioning of the user within <see cref="all"/>. | |
| /// | |
| /// Note that the index of a user may change as users are added and removed. | |
| /// </remarks> | |
| /// <seealso cref="all"/> | |
| public int index | |
| { | |
| get | |
| { | |
| if (m_Id == InvalidId) | |
| throw new InvalidOperationException("Invalid user"); | |
| var userIndex = TryFindUserIndex(m_Id); | |
| if (userIndex == -1) | |
| throw new InvalidOperationException($"User with ID {m_Id} is no longer valid"); | |
| return userIndex; | |
| } | |
| } | |
| /// <summary> | |
| /// The unique numeric ID of the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// The ID of a user is internally assigned and cannot be changed over its lifetime. No two users, even | |
| /// if not concurrently active, will receive the same ID. | |
| /// | |
| /// The ID stays valid and unique even if the user is removed and no longer <see cref="valid"/>. | |
| /// </remarks> | |
| public uint id => m_Id; | |
| ////TODO: bring documentation for these back when user management is implemented on Xbox and PS | |
| public InputUserAccountHandle? platformUserAccountHandle => s_GlobalState.allUserData[index].platformUserAccountHandle; | |
| public string platformUserAccountName => s_GlobalState.allUserData[index].platformUserAccountName; | |
| public string platformUserAccountId => s_GlobalState.allUserData[index].platformUserAccountId; | |
| ////REVIEW: Does it make sense to track used devices separately from paired devices? | |
| /// <summary> | |
| /// Devices assigned/paired/linked to the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// It is generally valid for a device to be assigned to multiple users. For example, two users could | |
| /// both use the local keyboard in a split-keyboard or hot seat setup. However, a platform may restrict this | |
| /// and mandate that a device never belong to more than one user. This is the case on Xbox and PS4, for | |
| /// example. | |
| /// | |
| /// To associate devices with users, use <see cref="PerformPairingWithDevice"/>. To remove devices, use | |
| /// <see cref="UnpairDevice"/> or <see cref="UnpairDevicesAndRemoveUser"/>. | |
| /// | |
| /// The array will be empty for a user who is currently not paired to any devices. | |
| /// | |
| /// If <see cref="actions"/> is set (<see cref="AssociateActionsWithUser(IInputActionCollection)"/>), then | |
| /// <see cref="IInputActionCollection.devices"/> will be kept synchronized with the devices paired to the user. | |
| /// </remarks> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| /// <seealso cref="UnpairDevice"/> | |
| /// <seealso cref="UnpairDevices"/> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| /// <seealso cref="InputUserChange.DevicePaired"/> | |
| /// <seealso cref="InputUserChange.DeviceUnpaired"/> | |
| public ReadOnlyArray<InputDevice> pairedDevices | |
| { | |
| get | |
| { | |
| var userIndex = index; | |
| return new ReadOnlyArray<InputDevice>(s_GlobalState.allPairedDevices, s_GlobalState.allUserData[userIndex].deviceStartIndex, | |
| s_GlobalState.allUserData[userIndex].deviceCount); | |
| } | |
| } | |
| /// <summary> | |
| /// Devices that were removed while they were still paired to the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// | |
| /// This list is cleared once the user has either regained lost devices or has regained other devices | |
| /// such that the <see cref="controlScheme"/> is satisfied. | |
| /// </remarks> | |
| /// <seealso cref="InputUserChange.DeviceRegained"/> | |
| /// <seealso cref="InputUserChange.DeviceLost"/> | |
| public ReadOnlyArray<InputDevice> lostDevices | |
| { | |
| get | |
| { | |
| var userIndex = index; | |
| return new ReadOnlyArray<InputDevice>(s_GlobalState.allLostDevices, s_GlobalState.allUserData[userIndex].lostDeviceStartIndex, | |
| s_GlobalState.allUserData[userIndex].lostDeviceCount); | |
| } | |
| } | |
| /// <summary> | |
| /// Actions associated with the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// Associating actions with a user will synchronize the actions with the devices paired to the | |
| /// user. Also, it makes it possible to use support for control scheme activation (<see | |
| /// cref="ActivateControlScheme(InputControlScheme)"/> and related APIs like <see cref="controlScheme"/> | |
| /// and <see cref="controlSchemeMatch"/>). | |
| /// | |
| /// Note that is generally does not make sense for users to share actions. Instead, each user should | |
| /// receive a set of actions private to the user. | |
| /// </remarks> | |
| /// <seealso cref="AssociateActionsWithUser(IInputActionCollection)"/> | |
| /// <seealso cref="InputActionMap"/> | |
| /// <seealso cref="InputActionAsset"/> | |
| /// <seealso cref="InputUserChange.ControlsChanged"/> | |
| public IInputActionCollection actions => s_GlobalState.allUserData[index].actions; | |
| /// <summary> | |
| /// The control scheme currently employed by the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// This is null by default. | |
| /// | |
| /// Any time the value of this property changes (whether by <see cref="ActivateControlScheme(string)"/> | |
| /// or by automatic switching), a notification is sent on <see cref="onChange"/> with | |
| /// <see cref="InputUserChange.ControlSchemeChanged"/>. | |
| /// | |
| /// Be aware that using control schemes with InputUsers requires <see cref="actions"/> to | |
| /// be set, i.e. input actions to be associated with the user (<see | |
| /// cref="AssociateActionsWithUser(IInputActionCollection)"/>). | |
| /// </remarks> | |
| /// <seealso cref="ActivateControlScheme(string)"/> | |
| /// <seealso cref="ActivateControlScheme(InputControlScheme)"/> | |
| /// <seealso cref="InputUserChange.ControlSchemeChanged"/> | |
| public InputControlScheme? controlScheme => s_GlobalState.allUserData[index].controlScheme; | |
| /// <summary> | |
| /// The result of matching the device requirements given by <see cref="controlScheme"/> against | |
| /// the devices paired to the user (<see cref="pairedDevices"/>). | |
| /// </summary> | |
| /// <remarks> | |
| /// When devices are paired to or unpaired from a user, as well as when a new control scheme is | |
| /// activated on a user, this property is updated automatically. | |
| /// </remarks> | |
| /// <seealso cref="InputControlScheme.deviceRequirements"/> | |
| /// <seealso cref="InputControlScheme.PickDevicesFrom{TDevices}"/> | |
| public InputControlScheme.MatchResult controlSchemeMatch => s_GlobalState.allUserData[index].controlSchemeMatch; | |
| /// <summary> | |
| /// Whether the user is missing devices required by the <see cref="controlScheme"/> activated | |
| /// on the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// This will only take required devices into account. Device requirements marked optional (<see | |
| /// cref="InputControlScheme.DeviceRequirement.isOptional"/>) will not be considered missing | |
| /// devices if they cannot be satisfied based on the devices paired to the user. | |
| /// </remarks> | |
| /// <seealso cref="InputControlScheme.deviceRequirements"/> | |
| public bool hasMissingRequiredDevices => s_GlobalState.allUserData[index].controlSchemeMatch.hasMissingRequiredDevices; | |
| /// <summary> | |
| /// List of all current users. | |
| /// </summary> | |
| /// <remarks> | |
| /// Use <see cref="PerformPairingWithDevice"/> to add new users and <see cref="UnpairDevicesAndRemoveUser"/> to | |
| /// remove users. | |
| /// | |
| /// Note that this array does not necessarily correspond to the list of users present at the platform level | |
| /// (e.g. Xbox and PS4). There can be users present at the platform level that are not present in this array | |
| /// (e.g. because they are not joined to the game) and users can even be present more than once (e.g. if | |
| /// playing on the user account but as two different players in the game). Also, there can be users in the array | |
| /// that are not present at the platform level. | |
| /// </remarks> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| public static ReadOnlyArray<InputUser> all => new ReadOnlyArray<InputUser>(s_GlobalState.allUsers, 0, s_GlobalState.allUserCount); | |
| /// <summary> | |
| /// Event that is triggered when the <see cref="InputUser">user</see> setup in the system | |
| /// changes. | |
| /// </summary> | |
| /// <remarks> | |
| /// Each notification receives the user that was affected by the change and, in the form of <see cref="InputUserChange"/>, | |
| /// a description of what has changed about the user. The third parameter may be null but if the change will be related | |
| /// to an input device, will reference the device involved in the change. | |
| /// </remarks> | |
| public static event Action<InputUser, InputUserChange, InputDevice> onChange | |
| { | |
| add | |
| { | |
| if (value == null) | |
| throw new ArgumentNullException(nameof(value)); | |
| s_GlobalState.onChange.AddCallback(value); | |
| } | |
| remove | |
| { | |
| if (value == null) | |
| throw new ArgumentNullException(nameof(value)); | |
| s_GlobalState.onChange.RemoveCallback(value); | |
| } | |
| } | |
| /// <summary> | |
| /// Event that is triggered when a device is used that is not currently paired to any user. | |
| /// </summary> | |
| /// <remarks> | |
| /// A device is considered "used" when it has magnitude (<see cref="InputControl.EvaluateMagnitude()"/>) greater than zero | |
| /// on a control that is not noisy (<see cref="InputControl.noisy"/>) and not synthetic (i.e. not a control that is | |
| /// "made up" like <see cref="Keyboard.anyKey"/>; <see cref="InputControl.synthetic"/>). | |
| /// | |
| /// Detecting the use of unpaired devices has a non-zero cost. While multiple levels of tests are applied to try to | |
| /// cheaply ignore devices that have events sent to them that do not contain user activity, finding out whether | |
| /// a device had real user activity will eventually require going through the device control by control. | |
| /// | |
| /// To enable detection of the use of unpaired devices, set <see cref="listenForUnpairedDeviceActivity"/> to true. | |
| /// It is disabled by default. | |
| /// | |
| /// The callback is invoked for each non-leaf, non-synthetic, non-noisy control that has been actuated on the device. | |
| /// It being restricted to non-leaf controls means that if, say, the stick on a gamepad is actuated in both X and Y | |
| /// direction, you will see two calls: one with stick/x and one with stick/y. | |
| /// | |
| /// The reason that the callback is invoked for each individual control is that pairing often relies on checking | |
| /// for specific kinds of interactions. For example, a pairing callback may listen exclusively for button presses. | |
| /// | |
| /// Note that whether the use of unpaired devices leads to them getting paired is under the control of the application. | |
| /// If the device should be paired, invoke <see cref="PerformPairingWithDevice"/> from the callback. If you do so, | |
| /// no further callbacks will get triggered for other controls that may have been actuated in the same event. | |
| /// | |
| /// Be aware that the callback is fired <em>before</em> input is actually incorporated into the device (it is | |
| /// indirectly triggered from <see cref="InputSystem.onEvent"/>). This means at the time the callback is run, | |
| /// the state of the given device does not yet have the input that triggered the callback. For this reason, the | |
| /// callback receives a second argument that references the event from which the use of an unpaired device was | |
| /// detected. | |
| /// | |
| /// What this sequence allows is to make changes to the system before the input is processed. For example, an | |
| /// action that is enabled as part of the callback will subsequently respond to the input that triggered the | |
| /// callback. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// // Activate support for listening to device activity. | |
| /// ++InputUser.listenForUnpairedDeviceActivity; | |
| /// | |
| /// // When a button on an unpaired device is pressed, pair the device to a new | |
| /// // or existing user. | |
| /// InputUser.onUnpairedDeviceUsed += | |
| /// usedControl => | |
| /// { | |
| /// // Only react to button presses on unpaired devices. | |
| /// if (!(usedControl is ButtonControl)) | |
| /// return; | |
| /// | |
| /// // Pair the device to a user. | |
| /// InputUser.PerformPairingWithDevice(usedControl.device); | |
| /// }; | |
| /// </code> | |
| /// </example> | |
| /// | |
| /// Another possible use of the callback is for implementing automatic control scheme switching for a user such that | |
| /// the user can, for example, switch from keyboard&mouse to gamepad seamlessly by simply picking up the gamepad | |
| /// and starting to play. | |
| /// </remarks> | |
| public static event Action<InputControl, InputEventPtr> onUnpairedDeviceUsed | |
| { | |
| add | |
| { | |
| if (value == null) | |
| throw new ArgumentNullException(nameof(value)); | |
| s_GlobalState.onUnpairedDeviceUsed.AddCallback(value); | |
| if (s_GlobalState.listenForUnpairedDeviceActivity > 0) | |
| HookIntoEvents(); | |
| } | |
| remove | |
| { | |
| if (value == null) | |
| throw new ArgumentNullException(nameof(value)); | |
| s_GlobalState.onUnpairedDeviceUsed.RemoveCallback(value); | |
| if (s_GlobalState.onUnpairedDeviceUsed.length == 0) | |
| UnhookFromDeviceStateChange(); | |
| } | |
| } | |
| /// <summary> | |
| /// Callback that works in combination with <see cref="onUnpairedDeviceUsed"/>. If all callbacks | |
| /// added to this event return <c>false</c> for a | |
| /// </summary> | |
| /// <remarks> | |
| /// Checking a given event for activity of interest is relatively fast but is still costlier than | |
| /// not doing it all. In case only certain devices are of interest for <see cref="onUnpairedDeviceUsed"/>, | |
| /// this "pre-filter" can be used to quickly reject entire devices and thus skip looking closer at | |
| /// an event. | |
| /// | |
| /// The first argument is the <see cref="InputDevice"/> than an event has been received for. | |
| /// The second argument is the <see cref="InputEvent"/> that is being looked at. | |
| /// | |
| /// A callback should return <c>true</c> if it wants <see cref="onUnpairedDeviceUsed"/> to proceed | |
| /// looking at the event and should return <c>false</c> if the event should be skipped. | |
| /// | |
| /// If multiple callbacks are added to the event, it is enough for any single one callback | |
| /// to return <c>true</c> for the event to get looked at. | |
| /// </remarks> | |
| /// <seealso cref="onUnpairedDeviceUsed"/> | |
| /// <seealso cref="listenForUnpairedDeviceActivity"/> | |
| public static event Func<InputDevice, InputEventPtr, bool> onPrefilterUnpairedDeviceActivity | |
| { | |
| add | |
| { | |
| if (value == null) | |
| throw new ArgumentNullException(nameof(value)); | |
| s_GlobalState.onPreFilterUnpairedDeviceUsed.AddCallback(value); | |
| } | |
| remove | |
| { | |
| if (value == null) | |
| throw new ArgumentNullException(nameof(value)); | |
| s_GlobalState.onPreFilterUnpairedDeviceUsed.RemoveCallback(value); | |
| } | |
| } | |
| ////TODO: After 1.0, make this a simple bool API that *underneath* uses a counter rather than exposing the counter | |
| //// directly to the user. | |
| /// <summary> | |
| /// Whether to listen for user activity on currently unpaired devices and invoke <see cref="onUnpairedDeviceUsed"/> | |
| /// if such activity is detected. | |
| /// </summary> | |
| /// <remarks> | |
| /// This is off by default. | |
| /// | |
| /// Note that enabling this has a non-zero cost. Whenever the state changes of a device that is not currently paired | |
| /// to a user, the system has to spend time figuring out whether there was a meaningful change or whether it's just | |
| /// noise on the device. | |
| /// | |
| /// This is an integer rather than a bool to allow multiple systems to concurrently use to listen for unpaired | |
| /// device activity without treading on each other when enabling/disabling the code path. | |
| /// </remarks> | |
| /// <seealso cref="onUnpairedDeviceUsed"/> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| public static int listenForUnpairedDeviceActivity | |
| { | |
| get => s_GlobalState.listenForUnpairedDeviceActivity; | |
| set | |
| { | |
| if (value < 0) | |
| throw new ArgumentOutOfRangeException(nameof(value), "Cannot be negative"); | |
| if (value > 0 && s_GlobalState.onUnpairedDeviceUsed.length > 0) | |
| HookIntoEvents(); | |
| else if (value == 0) | |
| UnhookFromDeviceStateChange(); | |
| s_GlobalState.listenForUnpairedDeviceActivity = value; | |
| } | |
| } | |
| public override string ToString() | |
| { | |
| if (!valid) | |
| return $"<Invalid> (id: {m_Id})"; | |
| var deviceList = string.Join(",", pairedDevices); | |
| return $"User #{index} (id: {m_Id}, devices: {deviceList}, actions: {actions})"; | |
| } | |
| /// <summary> | |
| /// Associate a collection of <see cref="InputAction"/>s with the user. | |
| /// </summary> | |
| /// <param name="actions">Actions to associate with the user, either an <see cref="InputActionAsset"/> | |
| /// or an <see cref="InputActionMap"/>. Can be <c>null</c> to unset the current association.</param> | |
| /// <exception cref="InvalidOperationException">The user instance is invalid.</exception> | |
| /// <remarks> | |
| /// Associating actions with a user will ensure that the <see cref="IInputActionCollection.devices"/> and | |
| /// <see cref="IInputActionCollection.bindingMask"/> property of the action collection are automatically | |
| /// kept in sync with the device paired to the user (see <see cref="pairedDevices"/>) and the control | |
| /// scheme active on the user (see <see cref="controlScheme"/>). | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var gamepad = Gamepad.all[0]; | |
| /// | |
| /// // Pair the gamepad to a user. | |
| /// var user = InputUser.PerformPairingWithDevice(gamepad); | |
| /// | |
| /// // Create an action map with an action. | |
| /// var actionMap = new InputActionMap(): | |
| /// actionMap.AddAction("Fire", binding: "<Gamepad>/buttonSouth"); | |
| /// | |
| /// // Associate the action map with the user (the same works for an asset). | |
| /// user.AssociateActionsWithUser(actionMap); | |
| /// | |
| /// // Now the action map is restricted to just the gamepad that is paired | |
| /// // with the user, even if there are more gamepads currently connected. | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="actions"/> | |
| public void AssociateActionsWithUser(IInputActionCollection actions) | |
| { | |
| var userIndex = index; // Throws if user is invalid. | |
| if (s_GlobalState.allUserData[userIndex].actions == actions) | |
| return; | |
| // If we already had actions associated, reset the binding mask and device list. | |
| var oldActions = s_GlobalState.allUserData[userIndex].actions; | |
| if (oldActions != null) | |
| { | |
| oldActions.devices = null; | |
| oldActions.bindingMask = null; | |
| } | |
| s_GlobalState.allUserData[userIndex].actions = actions; | |
| // If we've switched to a different set of actions, synchronize our state. | |
| if (actions != null) | |
| { | |
| HookIntoActionChange(); | |
| actions.devices = pairedDevices; | |
| if (s_GlobalState.allUserData[userIndex].controlScheme != null) | |
| ActivateControlSchemeInternal(userIndex, s_GlobalState.allUserData[userIndex].controlScheme.Value); | |
| } | |
| } | |
| public ControlSchemeChangeSyntax ActivateControlScheme(string schemeName) | |
| { | |
| // Look up control scheme by name in actions. | |
| if (!string.IsNullOrEmpty(schemeName)) | |
| { | |
| FindControlScheme(schemeName, out InputControlScheme scheme); // throws if not found | |
| return ActivateControlScheme(scheme); | |
| } | |
| return ActivateControlScheme(new InputControlScheme()); | |
| } | |
| private bool TryFindControlScheme(string schemeName, out InputControlScheme scheme) | |
| { | |
| if (string.IsNullOrEmpty(schemeName)) | |
| { | |
| scheme = default; | |
| return false; | |
| } | |
| // Need actions to be available to be able to activate control schemes by name only. | |
| if (s_GlobalState.allUserData[index].actions == null) | |
| throw new InvalidOperationException( | |
| $"Cannot set control scheme '{schemeName}' by name on user #{index} as not actions have been associated with the user yet (AssociateActionsWithUser)"); | |
| // Attempt to find control scheme by name | |
| var controlSchemes = s_GlobalState.allUserData[index].actions.controlSchemes; | |
| for (var i = 0; i < controlSchemes.Count; ++i) | |
| { | |
| if (string.Compare(controlSchemes[i].name, schemeName, | |
| StringComparison.InvariantCultureIgnoreCase) == 0) | |
| { | |
| scheme = controlSchemes[i]; | |
| return true; | |
| } | |
| } | |
| scheme = default; | |
| return false; | |
| } | |
| internal void FindControlScheme(string schemeName, out InputControlScheme scheme) | |
| { | |
| if (TryFindControlScheme(schemeName, out scheme)) | |
| return; | |
| throw new ArgumentException( | |
| $"Cannot find control scheme '{schemeName}' in actions '{s_GlobalState.allUserData[index].actions}'"); | |
| } | |
| public ControlSchemeChangeSyntax ActivateControlScheme(InputControlScheme scheme) | |
| { | |
| var userIndex = index; // Throws if user is invalid. | |
| if (s_GlobalState.allUserData[userIndex].controlScheme != scheme || | |
| (scheme == default && s_GlobalState.allUserData[userIndex].controlScheme != null)) | |
| { | |
| ActivateControlSchemeInternal(userIndex, scheme); | |
| Notify(userIndex, InputUserChange.ControlSchemeChanged, null); | |
| } | |
| return new ControlSchemeChangeSyntax { m_UserIndex = userIndex }; | |
| } | |
| private void ActivateControlSchemeInternal(int userIndex, InputControlScheme scheme) | |
| { | |
| var isEmpty = scheme == default; | |
| if (isEmpty) | |
| s_GlobalState.allUserData[userIndex].controlScheme = null; | |
| else | |
| s_GlobalState.allUserData[userIndex].controlScheme = scheme; | |
| if (s_GlobalState.allUserData[userIndex].actions != null) | |
| { | |
| if (isEmpty) | |
| { | |
| s_GlobalState.allUserData[userIndex].actions.bindingMask = null; | |
| s_GlobalState.allUserData[userIndex].controlSchemeMatch.Dispose(); | |
| s_GlobalState.allUserData[userIndex].controlSchemeMatch = new InputControlScheme.MatchResult(); | |
| } | |
| else | |
| { | |
| s_GlobalState.allUserData[userIndex].actions.bindingMask = new InputBinding { groups = scheme.bindingGroup }; | |
| UpdateControlSchemeMatch(userIndex); | |
| // If we had lost some devices, flush the list. We haven't regained the device | |
| // but we're no longer missing devices to play. | |
| if (s_GlobalState.allUserData[userIndex].controlSchemeMatch.isSuccessfulMatch) | |
| RemoveLostDevicesForUser(userIndex); | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Unpair a single device from the user. | |
| /// </summary> | |
| /// <param name="device">Device to unpair from the user. If the device is not currently paired to the user, | |
| /// the method does nothing.</param> | |
| /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception> | |
| /// <remarks> | |
| /// If actions are associated with the user (<see cref="actions"/>), the list of devices used by the | |
| /// actions (<see cref="IInputActionCollection.devices"/>) is automatically updated. | |
| /// | |
| /// If a control scheme is activated on the user (<see cref="controlScheme"/>), <see cref="controlSchemeMatch"/> | |
| /// is automatically updated. | |
| /// | |
| /// Sends <see cref="InputUserChange.DeviceUnpaired"/> through <see cref="onChange"/>. | |
| /// </remarks> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="UnpairDevices"/> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| /// <seealso cref="InputUserChange.DeviceUnpaired"/> | |
| public void UnpairDevice(InputDevice device) | |
| { | |
| if (device == null) | |
| throw new ArgumentNullException(nameof(device)); | |
| var userIndex = index; // Throws if user is invalid. | |
| // Ignore if not currently paired to user. | |
| if (!pairedDevices.ContainsReference(device)) | |
| return; | |
| RemoveDeviceFromUser(userIndex, device); | |
| } | |
| /// <summary> | |
| /// Unpair all devices from the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// If actions are associated with the user (<see cref="actions"/>), the list of devices used by the | |
| /// actions (<see cref="IInputActionCollection.devices"/>) is automatically updated. | |
| /// | |
| /// If a control scheme is activated on the user (<see cref="controlScheme"/>), <see cref="controlSchemeMatch"/> | |
| /// is automatically updated. | |
| /// | |
| /// Sends <see cref="InputUserChange.DeviceUnpaired"/> through <see cref="onChange"/> for every device | |
| /// unpaired from the user. | |
| /// </remarks> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="UnpairDevice"/> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| /// <seealso cref="InputUserChange.DeviceUnpaired"/> | |
| public void UnpairDevices() | |
| { | |
| var userIndex = index; // Throws if user is invalid. | |
| RemoveLostDevicesForUser(userIndex); | |
| using (InputActionRebindingExtensions.DeferBindingResolution()) | |
| { | |
| // We could remove the devices in bulk here but we still have to notify one | |
| // by one which ends up being more complicated than just unpairing the devices | |
| // individually here. | |
| while (s_GlobalState.allUserData[userIndex].deviceCount > 0) | |
| UnpairDevice(s_GlobalState.allPairedDevices[s_GlobalState.allUserData[userIndex].deviceStartIndex + s_GlobalState.allUserData[userIndex].deviceCount - 1]); | |
| } | |
| // Update control scheme, if necessary. | |
| if (s_GlobalState.allUserData[userIndex].controlScheme != null) | |
| UpdateControlSchemeMatch(userIndex); | |
| } | |
| private static void RemoveLostDevicesForUser(int userIndex) | |
| { | |
| var lostDeviceCount = s_GlobalState.allUserData[userIndex].lostDeviceCount; | |
| if (lostDeviceCount > 0) | |
| { | |
| var lostDeviceStartIndex = s_GlobalState.allUserData[userIndex].lostDeviceStartIndex; | |
| ArrayHelpers.EraseSliceWithCapacity(ref s_GlobalState.allLostDevices, ref s_GlobalState.allLostDeviceCount, | |
| lostDeviceStartIndex, lostDeviceCount); | |
| s_GlobalState.allUserData[userIndex].lostDeviceCount = 0; | |
| s_GlobalState.allUserData[userIndex].lostDeviceStartIndex = 0; | |
| // Adjust indices of other users. | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| if (s_GlobalState.allUserData[i].lostDeviceStartIndex > lostDeviceStartIndex) | |
| s_GlobalState.allUserData[i].lostDeviceStartIndex -= lostDeviceCount; | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Unpair all devices from the user and remove the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// If actions are associated with the user (<see cref="actions"/>), the list of devices used by the | |
| /// actions (<see cref="IInputActionCollection.devices"/>) is reset as is the binding mask (<see | |
| /// cref="IInputActionCollection.bindingMask"/>) in case a control scheme is activated on the user. | |
| /// | |
| /// Sends <see cref="InputUserChange.DeviceUnpaired"/> through <see cref="onChange"/> for every device | |
| /// unpaired from the user. | |
| /// | |
| /// Sends <see cref="InputUserChange.Removed"/>. | |
| /// </remarks> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="UnpairDevice"/> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| /// <seealso cref="InputUserChange.DeviceUnpaired"/> | |
| /// <seealso cref="InputUserChange.Removed"/> | |
| public void UnpairDevicesAndRemoveUser() | |
| { | |
| UnpairDevices(); | |
| var userIndex = index; | |
| RemoveUser(userIndex); | |
| m_Id = default; | |
| } | |
| /// <summary> | |
| /// Return a list of all currently added devices that are not paired to any user. | |
| /// </summary> | |
| /// <returns>A (possibly empty) list of devices that are currently not paired to a user.</returns> | |
| /// <remarks> | |
| /// The resulting list uses <see cref="Allocator.Temp"> temporary, unmanaged memory</see>. If not disposed of | |
| /// explicitly, the list will automatically be deallocated at the end of the frame and will become unusable. | |
| /// </remarks> | |
| /// <seealso cref="InputSystem.devices"/> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| public static InputControlList<InputDevice> GetUnpairedInputDevices() | |
| { | |
| var list = new InputControlList<InputDevice>(Allocator.Temp); | |
| GetUnpairedInputDevices(ref list); | |
| return list; | |
| } | |
| /// <summary> | |
| /// Add all currently added devices that are not paired to any user to <paramref name="list"/>. | |
| /// </summary> | |
| /// <param name="list">List to add the devices to. Devices will be added to the end.</param> | |
| /// <returns>Number of devices added to <paramref name="list"/>.</returns> | |
| /// <seealso cref="InputSystem.devices"/> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| public static int GetUnpairedInputDevices(ref InputControlList<InputDevice> list) | |
| { | |
| var countBefore = list.Count; | |
| foreach (var device in InputSystem.devices) | |
| { | |
| // If it's in s_AllPairedDevices, there is *some* user that is using the device. | |
| // We don't care which one it is here. | |
| if (ArrayHelpers.ContainsReference(s_GlobalState.allPairedDevices, s_GlobalState.allPairedDeviceCount, device)) | |
| continue; | |
| list.Add(device); | |
| } | |
| return list.Count - countBefore; | |
| } | |
| /// <summary> | |
| /// Find the user (if any) that <paramref name="device"/> is currently paired to. | |
| /// </summary> | |
| /// <param name="device">An input device.</param> | |
| /// <returns>The user that <paramref name="device"/> is currently paired to or <c>null</c> if the device | |
| /// is not currently paired to an user.</returns> | |
| /// <remarks> | |
| /// Note that multiple users may be paired to the same device. If that is the case for <paramref name="device"/>, | |
| /// the method will return one of the users with no guarantee which one it is. | |
| /// | |
| /// To find all users paired to a device requires manually going through the list of users and their paired | |
| /// devices. | |
| /// </remarks> | |
| /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="PerformPairingWithDevice"/> | |
| public static InputUser? FindUserPairedToDevice(InputDevice device) | |
| { | |
| if (device == null) | |
| throw new ArgumentNullException(nameof(device)); | |
| var userIndex = TryFindUserIndex(device); | |
| if (userIndex == -1) | |
| return null; | |
| return s_GlobalState.allUsers[userIndex]; | |
| } | |
| public static InputUser? FindUserByAccount(InputUserAccountHandle platformUserAccountHandle) | |
| { | |
| if (platformUserAccountHandle == default(InputUserAccountHandle)) | |
| throw new ArgumentException("Empty platform user account handle", nameof(platformUserAccountHandle)); | |
| var userIndex = TryFindUserIndex(platformUserAccountHandle); | |
| if (userIndex == -1) | |
| return null; | |
| return s_GlobalState.allUsers[userIndex]; | |
| } | |
| public static InputUser CreateUserWithoutPairedDevices() | |
| { | |
| var userIndex = AddUser(); | |
| return s_GlobalState.allUsers[userIndex]; | |
| } | |
| ////REVIEW: allow re-adding a user through this method? | |
| /// <summary> | |
| /// Pair the given device to a user. | |
| /// </summary> | |
| /// <param name="device">Device to pair to a user.</param> | |
| /// <param name="user">Optional parameter. If given, instead of creating a new user to pair the device | |
| /// to, the device is paired to the given user.</param> | |
| /// <param name="options">Optional set of options to modify pairing behavior.</param> | |
| /// <remarks> | |
| /// By default, a new user is created and <paramref name="device"/> is added <see cref="pairedDevices"/> | |
| /// of the user and <see cref="InputUserChange.DevicePaired"/> is sent on <see cref="onChange"/>. | |
| /// | |
| /// If a valid user is supplied to <paramref name="user"/>, the device is paired to the given user instead | |
| /// of creating a new user. By default, the device is added to the list of already paired devices for the user. | |
| /// This can be changed by using <see cref="InputUserPairingOptions.UnpairCurrentDevicesFromUser"/> which causes | |
| /// devices currently paired to the user to first be unpaired. | |
| /// | |
| /// The method will not prevent pairing of the same device to multiple users. | |
| /// | |
| /// Note that if the user has an associated set of actions (<see cref="actions"/>), the list of devices on the | |
| /// actions (<see cref="IInputActionCollection.devices"/>) will automatically be updated meaning that the newly | |
| /// paired devices will automatically reflect in the set of devices available to the user's actions. If the | |
| /// user has a control scheme that is currently activated (<see cref="controlScheme"/>), then <see cref="controlSchemeMatch"/> | |
| /// will also automatically update to reflect the matching of devices to the control scheme's device requirements. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// // Pair device to new user. | |
| /// var user = InputUser.PerformPairingWithDevice(wand1); | |
| /// | |
| /// // Pair another device to the same user. | |
| /// InputUser.PerformPairingWithDevice(wand2, user: user); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="pairedDevices"/> | |
| /// <seealso cref="UnpairDevice"/> | |
| /// <seealso cref="UnpairDevices"/> | |
| /// <seealso cref="UnpairDevicesAndRemoveUser"/> | |
| /// <seealso cref="InputUserChange.DevicePaired"/> | |
| public static InputUser PerformPairingWithDevice(InputDevice device, | |
| InputUser user = default, | |
| InputUserPairingOptions options = InputUserPairingOptions.None) | |
| { | |
| if (device == null) | |
| throw new ArgumentNullException(nameof(device)); | |
| if (user != default && !user.valid) | |
| throw new ArgumentException("Invalid user", nameof(user)); | |
| // Create new user, if needed. | |
| int userIndex; | |
| if (user == default) | |
| { | |
| userIndex = AddUser(); | |
| } | |
| else | |
| { | |
| // We have an existing user. | |
| userIndex = user.index; | |
| // See if we're supposed to clear out the user's currently paired devices first. | |
| if ((options & InputUserPairingOptions.UnpairCurrentDevicesFromUser) != 0) | |
| user.UnpairDevices(); | |
| // Ignore call if device is already paired to user. | |
| if (user.pairedDevices.ContainsReference(device)) | |
| { | |
| // Still might have to initiate user account selection. | |
| if ((options & InputUserPairingOptions.ForcePlatformUserAccountSelection) != 0) | |
| InitiateUserAccountSelection(userIndex, device, options); | |
| return user; | |
| } | |
| } | |
| // Handle the user account side of pairing. | |
| var accountSelectionInProgress = InitiateUserAccountSelection(userIndex, device, options); | |
| // Except if we have initiate user account selection, pair the device to | |
| // to the user now. | |
| if (!accountSelectionInProgress) | |
| AddDeviceToUser(userIndex, device); | |
| return s_GlobalState.allUsers[userIndex]; | |
| } | |
| private static bool InitiateUserAccountSelection(int userIndex, InputDevice device, | |
| InputUserPairingOptions options) | |
| { | |
| // See if there's a platform user account we can get from the device. | |
| // NOTE: We don't query the current user account if the caller has opted to force account selection. | |
| var queryUserAccountResult = | |
| (options & InputUserPairingOptions.ForcePlatformUserAccountSelection) == 0 | |
| ? UpdatePlatformUserAccount(userIndex, device) | |
| : 0; | |
| ////REVIEW: what should we do if there already is an account selection in progress? InvalidOperationException? | |
| // If the device supports user account selection but we didn't get one, | |
| // try to initiate account selection. | |
| if ((options & InputUserPairingOptions.ForcePlatformUserAccountSelection) != 0 || | |
| (queryUserAccountResult != InputDeviceCommand.GenericFailure && | |
| (queryUserAccountResult & (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount) == 0 && | |
| (options & InputUserPairingOptions.ForceNoPlatformUserAccountSelection) == 0)) | |
| { | |
| if (InitiateUserAccountSelectionAtPlatformLevel(device)) | |
| { | |
| s_GlobalState.allUserData[userIndex].flags |= UserFlags.UserAccountSelectionInProgress; | |
| s_GlobalState.ongoingAccountSelections.Append( | |
| new OngoingAccountSelection | |
| { | |
| device = device, | |
| userId = s_GlobalState.allUsers[userIndex].id, | |
| }); | |
| // Make sure we receive a notification for the configuration event. | |
| HookIntoDeviceChange(); | |
| // Tell listeners that we started an account selection. | |
| Notify(userIndex, InputUserChange.AccountSelectionInProgress, device); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| public bool Equals(InputUser other) | |
| { | |
| return m_Id == other.m_Id; | |
| } | |
| public override bool Equals(object obj) | |
| { | |
| if (ReferenceEquals(null, obj)) | |
| return false; | |
| return obj is InputUser && Equals((InputUser)obj); | |
| } | |
| public override int GetHashCode() | |
| { | |
| return (int)m_Id; | |
| } | |
| public static bool operator==(InputUser left, InputUser right) | |
| { | |
| return left.m_Id == right.m_Id; | |
| } | |
| public static bool operator!=(InputUser left, InputUser right) | |
| { | |
| return left.m_Id != right.m_Id; | |
| } | |
| /// <summary> | |
| /// Add a new user. | |
| /// </summary> | |
| /// <returns>Index of the newly created user.</returns> | |
| /// <remarks> | |
| /// Adding a user sends a notification with <see cref="InputUserChange.Added"/> through <see cref="onChange"/>. | |
| /// | |
| /// The user will start out with no devices and no actions assigned. | |
| /// | |
| /// The user is added to <see cref="all"/>. | |
| /// </remarks> | |
| private static int AddUser() | |
| { | |
| var id = ++s_GlobalState.lastUserId; | |
| // Add to list. | |
| var userCount = s_GlobalState.allUserCount; | |
| ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allUsers, ref userCount, new InputUser { m_Id = id }); | |
| var userIndex = ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allUserData, ref s_GlobalState.allUserCount, new UserData()); | |
| // Send notification. | |
| Notify(userIndex, InputUserChange.Added, null); | |
| return userIndex; | |
| } | |
| /// <summary> | |
| /// Remove an active user. | |
| /// </summary> | |
| /// <param name="userIndex">Index of active user.</param> | |
| /// <remarks> | |
| /// Removing a user also unassigns all currently assigned devices from the user. On completion of this | |
| /// method, <see cref="pairedDevices"/> of <paramref name="user"/> will be empty. | |
| /// </remarks> | |
| private static void RemoveUser(int userIndex) | |
| { | |
| Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid"); | |
| Debug.Assert(s_GlobalState.allUserData[userIndex].deviceCount == 0, "User must not have paired devices still"); | |
| // Reset data from control scheme. | |
| if (s_GlobalState.allUserData[userIndex].controlScheme != null) | |
| { | |
| if (s_GlobalState.allUserData[userIndex].actions != null) | |
| s_GlobalState.allUserData[userIndex].actions.bindingMask = null; | |
| } | |
| s_GlobalState.allUserData[userIndex].controlSchemeMatch.Dispose(); | |
| // Remove lost devices. | |
| RemoveLostDevicesForUser(userIndex); | |
| // Remove account selections that are in progress. | |
| for (var i = 0; i < s_GlobalState.ongoingAccountSelections.length; ++i) | |
| { | |
| if (s_GlobalState.ongoingAccountSelections[i].userId != s_GlobalState.allUsers[userIndex].id) | |
| continue; | |
| s_GlobalState.ongoingAccountSelections.RemoveAtByMovingTailWithCapacity(i); | |
| --i; | |
| } | |
| // Send notification (do before we actually remove the user). | |
| Notify(userIndex, InputUserChange.Removed, null); | |
| // Remove. | |
| var userCount = s_GlobalState.allUserCount; | |
| s_GlobalState.allUsers.EraseAtWithCapacity(ref userCount, userIndex); | |
| s_GlobalState.allUserData.EraseAtWithCapacity(ref s_GlobalState.allUserCount, userIndex); | |
| // Remove our hook if we no longer need it. | |
| if (s_GlobalState.allUserCount == 0) | |
| { | |
| UnhookFromDeviceChange(); | |
| UnhookFromActionChange(); | |
| } | |
| } | |
| private static void Notify(int userIndex, InputUserChange change, InputDevice device) | |
| { | |
| Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid"); | |
| if (s_GlobalState.onChange.length == 0) | |
| return; | |
| Profiler.BeginSample("InputUser.onChange"); | |
| s_GlobalState.onChange.LockForChanges(); | |
| for (var i = 0; i < s_GlobalState.onChange.length; ++i) | |
| { | |
| try | |
| { | |
| s_GlobalState.onChange[i](s_GlobalState.allUsers[userIndex], change, device); | |
| } | |
| catch (Exception exception) | |
| { | |
| Debug.LogError($"{exception.GetType().Name} while executing 'InputUser.onChange' callbacks"); | |
| Debug.LogException(exception); | |
| } | |
| } | |
| s_GlobalState.onChange.UnlockForChanges(); | |
| Profiler.EndSample(); | |
| } | |
| private static int TryFindUserIndex(uint userId) | |
| { | |
| Debug.Assert(userId != InvalidId, "User ID is invalid"); | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| if (s_GlobalState.allUsers[i].m_Id == userId) | |
| return i; | |
| } | |
| return -1; | |
| } | |
| private static int TryFindUserIndex(InputUserAccountHandle platformHandle) | |
| { | |
| Debug.Assert(platformHandle != default, "User platform handle is invalid"); | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| if (s_GlobalState.allUserData[i].platformUserAccountHandle == platformHandle) | |
| return i; | |
| } | |
| return -1; | |
| } | |
| /// <summary> | |
| /// Find the user (if any) that is currently assigned the given <paramref name="device"/>. | |
| /// </summary> | |
| /// <param name="device">An input device that has been added to the system.</param> | |
| /// <returns>Index of the user that has <paramref name="device"/> among its <see cref="pairedDevices"/> or -1 if | |
| /// no user is currently assigned the given device.</returns> | |
| private static int TryFindUserIndex(InputDevice device) | |
| { | |
| Debug.Assert(device != null, "Device cannot be null"); | |
| var indexOfDevice = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount); | |
| if (indexOfDevice == -1) | |
| return -1; | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| var startIndex = s_GlobalState.allUserData[i].deviceStartIndex; | |
| if (startIndex <= indexOfDevice && indexOfDevice < startIndex + s_GlobalState.allUserData[i].deviceCount) | |
| return i; | |
| } | |
| return -1; | |
| } | |
| /// <summary> | |
| /// Add the given device to the user as either a lost device or a paired device. | |
| /// </summary> | |
| /// <param name="userIndex"></param> | |
| /// <param name="device"></param> | |
| /// <param name="asLostDevice"></param> | |
| private static void AddDeviceToUser(int userIndex, InputDevice device, bool asLostDevice = false, bool dontUpdateControlScheme = false) | |
| { | |
| Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid"); | |
| Debug.Assert(device != null, "Device cannot be null"); | |
| if (asLostDevice) | |
| Debug.Assert(!s_GlobalState.allUsers[userIndex].lostDevices.ContainsReference(device), "Device already in set of lostDevices for user"); | |
| else | |
| Debug.Assert(!s_GlobalState.allUsers[userIndex].pairedDevices.ContainsReference(device), "Device already in set of pairedDevices for user"); | |
| var deviceCount = asLostDevice | |
| ? s_GlobalState.allUserData[userIndex].lostDeviceCount | |
| : s_GlobalState.allUserData[userIndex].deviceCount; | |
| var deviceStartIndex = asLostDevice | |
| ? s_GlobalState.allUserData[userIndex].lostDeviceStartIndex | |
| : s_GlobalState.allUserData[userIndex].deviceStartIndex; | |
| ++s_GlobalState.pairingStateVersion; | |
| // Move our devices to end of array. | |
| if (deviceCount > 0) | |
| { | |
| ArrayHelpers.MoveSlice(asLostDevice ? s_GlobalState.allLostDevices : s_GlobalState.allPairedDevices, deviceStartIndex, | |
| asLostDevice ? s_GlobalState.allLostDeviceCount - deviceCount : s_GlobalState.allPairedDeviceCount - deviceCount, | |
| deviceCount); | |
| // Adjust users that have been impacted by the change. | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| if (i == userIndex) | |
| continue; | |
| if ((asLostDevice ? s_GlobalState.allUserData[i].lostDeviceStartIndex : s_GlobalState.allUserData[i].deviceStartIndex) <= deviceStartIndex) | |
| continue; | |
| if (asLostDevice) | |
| s_GlobalState.allUserData[i].lostDeviceStartIndex -= deviceCount; | |
| else | |
| s_GlobalState.allUserData[i].deviceStartIndex -= deviceCount; | |
| } | |
| } | |
| // Append to array. | |
| if (asLostDevice) | |
| { | |
| s_GlobalState.allUserData[userIndex].lostDeviceStartIndex = s_GlobalState.allLostDeviceCount - deviceCount; | |
| ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allLostDevices, ref s_GlobalState.allLostDeviceCount, device); | |
| ++s_GlobalState.allUserData[userIndex].lostDeviceCount; | |
| } | |
| else | |
| { | |
| s_GlobalState.allUserData[userIndex].deviceStartIndex = s_GlobalState.allPairedDeviceCount - deviceCount; | |
| ArrayHelpers.AppendWithCapacity(ref s_GlobalState.allPairedDevices, ref s_GlobalState.allPairedDeviceCount, device); | |
| ++s_GlobalState.allUserData[userIndex].deviceCount; | |
| // If the user has actions, sync the devices on them with what we have now. | |
| var actions = s_GlobalState.allUserData[userIndex].actions; | |
| if (actions != null) | |
| { | |
| actions.devices = s_GlobalState.allUsers[userIndex].pairedDevices; | |
| // Also, if we have a control scheme, update the matching of device requirements | |
| // against the device we now have. | |
| if (!dontUpdateControlScheme && s_GlobalState.allUserData[userIndex].controlScheme != null) | |
| UpdateControlSchemeMatch(userIndex); | |
| } | |
| } | |
| // Make sure we get OnDeviceChange notifications. | |
| HookIntoDeviceChange(); | |
| // Let listeners know. | |
| Notify(userIndex, asLostDevice ? InputUserChange.DeviceLost : InputUserChange.DevicePaired, device); | |
| } | |
| private static void RemoveDeviceFromUser(int userIndex, InputDevice device, bool asLostDevice = false) | |
| { | |
| Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid"); | |
| Debug.Assert(device != null, "Device cannot be null"); | |
| var deviceIndex = asLostDevice | |
| ? s_GlobalState.allLostDevices.IndexOfReference(device, s_GlobalState.allLostDeviceCount) | |
| : s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allUserData[userIndex].deviceStartIndex, | |
| s_GlobalState.allUserData[userIndex].deviceCount); | |
| if (deviceIndex == -1) | |
| { | |
| // Device not in list. Ignore. | |
| return; | |
| } | |
| if (asLostDevice) | |
| { | |
| s_GlobalState.allLostDevices.EraseAtWithCapacity(ref s_GlobalState.allLostDeviceCount, deviceIndex); | |
| --s_GlobalState.allUserData[userIndex].lostDeviceCount; | |
| } | |
| else | |
| { | |
| ++s_GlobalState.pairingStateVersion; | |
| s_GlobalState.allPairedDevices.EraseAtWithCapacity(ref s_GlobalState.allPairedDeviceCount, deviceIndex); | |
| --s_GlobalState.allUserData[userIndex].deviceCount; | |
| } | |
| // Adjust indices of other users. | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| if ((asLostDevice ? s_GlobalState.allUserData[i].lostDeviceStartIndex : s_GlobalState.allUserData[i].deviceStartIndex) <= deviceIndex) | |
| continue; | |
| if (asLostDevice) | |
| --s_GlobalState.allUserData[i].lostDeviceStartIndex; | |
| else | |
| --s_GlobalState.allUserData[i].deviceStartIndex; | |
| } | |
| if (!asLostDevice) | |
| { | |
| // Remove any ongoing account selections for the user on the given device. | |
| for (var i = 0; i < s_GlobalState.ongoingAccountSelections.length; ++i) | |
| { | |
| if (s_GlobalState.ongoingAccountSelections[i].userId != s_GlobalState.allUsers[userIndex].id || | |
| s_GlobalState.ongoingAccountSelections[i].device != device) | |
| continue; | |
| s_GlobalState.ongoingAccountSelections.RemoveAtByMovingTailWithCapacity(i); | |
| --i; | |
| } | |
| // If the user has actions, sync the devices on them with what we have now. | |
| var actions = s_GlobalState.allUserData[userIndex].actions; | |
| if (actions != null) | |
| { | |
| actions.devices = s_GlobalState.allUsers[userIndex].pairedDevices; | |
| if (s_GlobalState.allUsers[userIndex].controlScheme != null) | |
| UpdateControlSchemeMatch(userIndex); | |
| } | |
| // Notify listeners. | |
| Notify(userIndex, InputUserChange.DeviceUnpaired, device); | |
| } | |
| } | |
| private static void UpdateControlSchemeMatch(int userIndex, bool autoPairMissing = false) | |
| { | |
| Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid"); | |
| // Nothing to do if we don't have a control scheme. | |
| if (s_GlobalState.allUserData[userIndex].controlScheme == null) | |
| return; | |
| // Get rid of last match result and start new match. | |
| s_GlobalState.allUserData[userIndex].controlSchemeMatch.Dispose(); | |
| var matchResult = new InputControlScheme.MatchResult(); | |
| try | |
| { | |
| // Match the control scheme's requirements against the devices paired to the user. | |
| var scheme = s_GlobalState.allUserData[userIndex].controlScheme.Value; | |
| if (scheme.deviceRequirements.Count > 0) | |
| { | |
| var availableDevices = new InputControlList<InputDevice>(Allocator.Temp); | |
| try | |
| { | |
| // Add devices already paired to user. | |
| availableDevices.AddSlice(s_GlobalState.allUsers[userIndex].pairedDevices); | |
| // If we're supposed to grab whatever additional devices we need from what's | |
| // available, add all unpaired devices to the list. | |
| // NOTE: These devices go *after* the devices already paired (if any) meaning that | |
| // the control scheme matching will grab already paired devices *first*. | |
| if (autoPairMissing) | |
| { | |
| var startIndex = availableDevices.Count; | |
| var count = GetUnpairedInputDevices(ref availableDevices); | |
| // We want to favor devices that are already assigned to the same platform user account. | |
| // Sort the unpaired devices we've added to the list such that the ones belonging to the | |
| // same user account come first. | |
| if (s_GlobalState.allUserData[userIndex].platformUserAccountHandle != null) | |
| availableDevices.Sort(startIndex, count, | |
| new CompareDevicesByUserAccount | |
| { | |
| platformUserAccountHandle = s_GlobalState.allUserData[userIndex].platformUserAccountHandle.Value | |
| }); | |
| } | |
| matchResult = scheme.PickDevicesFrom(availableDevices); | |
| if (matchResult.isSuccessfulMatch) | |
| { | |
| // Control scheme is satisfied with the devices we have available. | |
| // If we may have grabbed as of yet unpaired devices, go and pair them to the user. | |
| if (autoPairMissing) | |
| { | |
| // Update match result on user before potentially invoking callbacks. | |
| s_GlobalState.allUserData[userIndex].controlSchemeMatch = matchResult; | |
| foreach (var device in matchResult.devices) | |
| { | |
| // Skip if already paired to user. | |
| if (s_GlobalState.allUsers[userIndex].pairedDevices.ContainsReference(device)) | |
| continue; | |
| AddDeviceToUser(userIndex, device, dontUpdateControlScheme: true); | |
| } | |
| } | |
| } | |
| } | |
| finally | |
| { | |
| availableDevices.Dispose(); | |
| } | |
| } | |
| s_GlobalState.allUserData[userIndex].controlSchemeMatch = matchResult; | |
| } | |
| catch (Exception) | |
| { | |
| // If we had an exception and are bailing out, make sure we aren't leaking native memory | |
| // we allocated. | |
| matchResult.Dispose(); | |
| throw; | |
| } | |
| } | |
| private static long UpdatePlatformUserAccount(int userIndex, InputDevice device) | |
| { | |
| Debug.Assert(userIndex >= 0 && userIndex < s_GlobalState.allUserCount, "User index is invalid"); | |
| // Fetch account details from backend. | |
| var queryResult = QueryPairedPlatformUserAccount(device, out var platformUserAccountHandle, | |
| out var platformUserAccountName, out var platformUserAccountId); | |
| // Nothing much to do if not supported by device. | |
| if (queryResult == InputDeviceCommand.GenericFailure) | |
| { | |
| // Check if there's an account selection in progress. There shouldn't be as it's | |
| // weird for the device to no signal it does not support querying user account, but | |
| // just to be safe, we check. | |
| if ((s_GlobalState.allUserData[userIndex].flags & UserFlags.UserAccountSelectionInProgress) != 0) | |
| Notify(userIndex, InputUserChange.AccountSelectionCanceled, null); | |
| s_GlobalState.allUserData[userIndex].platformUserAccountHandle = null; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountName = null; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountId = null; | |
| return queryResult; | |
| } | |
| // Check if there's an account selection that we have initiated. | |
| if ((s_GlobalState.allUserData[userIndex].flags & UserFlags.UserAccountSelectionInProgress) != 0) | |
| { | |
| // Yes, there is. See if it is complete. | |
| if ((queryResult & (long)QueryPairedUserAccountCommand.Result.UserAccountSelectionInProgress) != 0) | |
| { | |
| // No, still in progress. | |
| } | |
| else if ((queryResult & (long)QueryPairedUserAccountCommand.Result.UserAccountSelectionCanceled) != 0) | |
| { | |
| // Got canceled. | |
| Notify(userIndex, InputUserChange.AccountSelectionCanceled, device); | |
| } | |
| else | |
| { | |
| // Yes, it is complete. | |
| s_GlobalState.allUserData[userIndex].flags &= ~UserFlags.UserAccountSelectionInProgress; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountHandle = platformUserAccountHandle; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountName = platformUserAccountName; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountId = platformUserAccountId; | |
| Notify(userIndex, InputUserChange.AccountSelectionComplete, device); | |
| } | |
| } | |
| // Check if user account details have changed. | |
| else if (s_GlobalState.allUserData[userIndex].platformUserAccountHandle != platformUserAccountHandle || | |
| s_GlobalState.allUserData[userIndex].platformUserAccountId != platformUserAccountId) | |
| { | |
| s_GlobalState.allUserData[userIndex].platformUserAccountHandle = platformUserAccountHandle; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountName = platformUserAccountName; | |
| s_GlobalState.allUserData[userIndex].platformUserAccountId = platformUserAccountId; | |
| Notify(userIndex, InputUserChange.AccountChanged, device); | |
| } | |
| else if (s_GlobalState.allUserData[userIndex].platformUserAccountName != platformUserAccountName) | |
| { | |
| Notify(userIndex, InputUserChange.AccountNameChanged, device); | |
| } | |
| return queryResult; | |
| } | |
| ////TODO: bring documentation for these back when user management is implemented on Xbox and PS | |
| private static long QueryPairedPlatformUserAccount(InputDevice device, | |
| out InputUserAccountHandle? platformAccountHandle, out string platformAccountName, out string platformAccountId) | |
| { | |
| Debug.Assert(device != null, "Device cannot be null"); | |
| // Query user account info from backend. | |
| var queryPairedUser = QueryPairedUserAccountCommand.Create(); | |
| var result = device.ExecuteCommand(ref queryPairedUser); | |
| if (result == InputDeviceCommand.GenericFailure) | |
| { | |
| // Not currently paired to user account in backend. | |
| platformAccountHandle = null; | |
| platformAccountName = null; | |
| platformAccountId = null; | |
| return InputDeviceCommand.GenericFailure; | |
| } | |
| // Success. There is a user account currently paired to the device and we now have the | |
| // platform's user account details. | |
| if ((result & (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount) != 0) | |
| { | |
| platformAccountHandle = | |
| new InputUserAccountHandle(device.description.interfaceName ?? "<Unknown>", queryPairedUser.handle); | |
| platformAccountName = queryPairedUser.name; | |
| platformAccountId = queryPairedUser.id; | |
| } | |
| else | |
| { | |
| // The device supports QueryPairedUserAccountCommand but reports that the | |
| // device is not currently paired to a user. | |
| // | |
| // NOTE: On Switch, where the system itself does not store account<->pairing, we will always | |
| // end up here until we've initiated an account selection through the backend itself. | |
| platformAccountHandle = null; | |
| platformAccountName = null; | |
| platformAccountId = null; | |
| } | |
| return result; | |
| } | |
| /// <summary> | |
| /// Try to initiate user account pairing for the given device at the platform level. | |
| /// </summary> | |
| /// <param name="device"></param> | |
| /// <returns>True if the device accepted the request and an account picker has been raised.</returns> | |
| /// <remarks> | |
| /// Sends <see cref="InitiateUserAccountPairingCommand"/> to the device. | |
| /// </remarks> | |
| private static bool InitiateUserAccountSelectionAtPlatformLevel(InputDevice device) | |
| { | |
| Debug.Assert(device != null, "Device cannot be null"); | |
| var initiateUserPairing = InitiateUserAccountPairingCommand.Create(); | |
| var initiatePairingResult = device.ExecuteCommand(ref initiateUserPairing); | |
| if (initiatePairingResult == (long)InitiateUserAccountPairingCommand.Result.ErrorAlreadyInProgress) | |
| throw new InvalidOperationException("User pairing already in progress"); | |
| return initiatePairingResult == (long)InitiateUserAccountPairingCommand.Result.SuccessfullyInitiated; | |
| } | |
| private static void OnActionChange(object obj, InputActionChange change) | |
| { | |
| if (change == InputActionChange.BoundControlsChanged) | |
| { | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| ref var user = ref s_GlobalState.allUsers[i]; | |
| if (ReferenceEquals(user.actions, obj)) | |
| Notify(i, InputUserChange.ControlsChanged, null); | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Invoked in response to <see cref="InputSystem.onDeviceChange"/>. | |
| /// </summary> | |
| /// <param name="device"></param> | |
| /// <param name="change"></param> | |
| /// <remarks> | |
| /// We monitor the device setup in the system for activity that impacts the user setup. | |
| /// </remarks> | |
| private static void OnDeviceChange(InputDevice device, InputDeviceChange change) | |
| { | |
| switch (change) | |
| { | |
| // Existing device removed. May mean a user has lost a device due to the battery running | |
| // out or the device being unplugged. | |
| // NOTE: We ignore Disconnected here. Removed is what gets sent whenever a device is taken off of | |
| // InputSystem.devices -- which is what we're interested in here. | |
| case InputDeviceChange.Removed: | |
| { | |
| // Could have been removed from multiple users. Repeatedly search in s_AllPairedDevices | |
| // until we can't find the device anymore. | |
| var deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount); | |
| while (deviceIndex != -1) | |
| { | |
| // Find user. Must be there as we found the device in s_AllPairedDevices. | |
| var userIndex = -1; | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| var deviceStartIndex = s_GlobalState.allUserData[i].deviceStartIndex; | |
| if (deviceStartIndex <= deviceIndex && deviceIndex < deviceStartIndex + s_GlobalState.allUserData[i].deviceCount) | |
| { | |
| userIndex = i; | |
| break; | |
| } | |
| } | |
| // Add device to list of lost devices. | |
| // NOTE: This will also send a DeviceLost notification. | |
| // NOTE: Temporarily the device is on both lists. | |
| AddDeviceToUser(userIndex, device, asLostDevice: true); | |
| // Remove it from the user. | |
| RemoveDeviceFromUser(userIndex, device); | |
| // Search for another user paired to the same device. | |
| deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount); | |
| } | |
| break; | |
| } | |
| // New device was added. See if it was a device we previously lost on a user. | |
| case InputDeviceChange.Added: | |
| { | |
| // Search all lost devices. Could affect multiple users. | |
| // Note that RemoveDeviceFromUser removes one element, hence no advancement of deviceIndex. | |
| for (var deviceIndex = FindLostDevice(device); deviceIndex != -1; | |
| deviceIndex = FindLostDevice(device, deviceIndex)) | |
| { | |
| // Find user. Must be there as we found the device in s_AllLostDevices. | |
| var userIndex = -1; | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| var deviceStartIndex = s_GlobalState.allUserData[i].lostDeviceStartIndex; | |
| if (deviceStartIndex <= deviceIndex && deviceIndex < deviceStartIndex + s_GlobalState.allUserData[i].lostDeviceCount) | |
| { | |
| userIndex = i; | |
| break; | |
| } | |
| } | |
| // Remove from list of lost devices. No notification. Notice that we need to use device | |
| // from lost device list even if its another instance. | |
| RemoveDeviceFromUser(userIndex, s_GlobalState.allLostDevices[deviceIndex], asLostDevice: true); | |
| // Notify. | |
| Notify(userIndex, InputUserChange.DeviceRegained, device); | |
| // Add back as normally paired device. | |
| AddDeviceToUser(userIndex, device); | |
| } | |
| break; | |
| } | |
| // Device had its configuration changed which may mean we have a different user account paired | |
| // to the device now. | |
| case InputDeviceChange.ConfigurationChanged: | |
| { | |
| // See if the this is a device that we were waiting for an account selection on. If so, pair | |
| // it to the user that was waiting. | |
| var wasOngoingAccountSelection = false; | |
| for (var i = 0; i < s_GlobalState.ongoingAccountSelections.length; ++i) | |
| { | |
| if (s_GlobalState.ongoingAccountSelections[i].device != device) | |
| continue; | |
| var userIndex = new InputUser { m_Id = s_GlobalState.ongoingAccountSelections[i].userId }.index; | |
| var queryResult = UpdatePlatformUserAccount(userIndex, device); | |
| if ((queryResult & (long)QueryPairedUserAccountCommand.Result.UserAccountSelectionInProgress) == 0) | |
| { | |
| wasOngoingAccountSelection = true; | |
| s_GlobalState.ongoingAccountSelections.RemoveAtByMovingTailWithCapacity(i); | |
| --i; | |
| // If the device wasn't paired to the user, pair it now. | |
| if (!s_GlobalState.allUsers[userIndex].pairedDevices.ContainsReference(device)) | |
| AddDeviceToUser(userIndex, device); | |
| } | |
| } | |
| // If it wasn't a configuration change event from an account selection, go and check whether | |
| // there was a user account change that happened outside the application. | |
| if (!wasOngoingAccountSelection) | |
| { | |
| // Could be paired to multiple users. Repeatedly search in s_AllPairedDevices | |
| // until we can't find the device anymore. | |
| var deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, s_GlobalState.allPairedDeviceCount); | |
| while (deviceIndex != -1) | |
| { | |
| // Find user. Must be there as we found the device in s_AllPairedDevices. | |
| var userIndex = -1; | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| { | |
| var deviceStartIndex = s_GlobalState.allUserData[i].deviceStartIndex; | |
| if (deviceStartIndex <= deviceIndex && deviceIndex < deviceStartIndex + s_GlobalState.allUserData[i].deviceCount) | |
| { | |
| userIndex = i; | |
| break; | |
| } | |
| } | |
| // Check user account. | |
| UpdatePlatformUserAccount(userIndex, device); | |
| // Search for another user paired to the same device. | |
| // Note that action is tied to user and hence we can skip to end of slice associated | |
| // with the current user or at least one element forward. | |
| var offsetNextSlice = deviceIndex + Math.Max(1, s_GlobalState.allUserData[userIndex].deviceCount); | |
| deviceIndex = s_GlobalState.allPairedDevices.IndexOfReference(device, offsetNextSlice, s_GlobalState.allPairedDeviceCount - offsetNextSlice); | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| private static int FindLostDevice(InputDevice device, int startIndex = 0) | |
| { | |
| // Compare both by device ID and by reference. We may be looking at a device that was recreated | |
| // due to layout changes (new InputDevice instance, same ID) or a device that was reconnected | |
| // and thus fetched out of `disconnectedDevices` (same InputDevice instance, new ID). | |
| var newDeviceId = device.deviceId; | |
| for (var i = startIndex; i < s_GlobalState.allLostDeviceCount; ++i) | |
| { | |
| var lostDevice = s_GlobalState.allLostDevices[i]; | |
| if (device == lostDevice || lostDevice.deviceId == newDeviceId) return i; | |
| } | |
| return -1; | |
| } | |
| // We hook this into InputSystem.onEvent when listening for activity on unpaired devices. | |
| // What this means is that we get to run *before* state reaches the device. This in turn | |
| // means that should the device get paired as a result, actions that are enabled as part | |
| // of the pairing will immediately get triggered. This would not be the case if we hook | |
| // into InputState.onDeviceChange instead which only triggers once state has been altered. | |
| // | |
| // NOTE: This also means that unpaired device activity will *only* be detected from events, | |
| // NOT from state changes applied directly through InputState.Change. | |
| private static void OnEvent(InputEventPtr eventPtr, InputDevice device) | |
| { | |
| Debug.Assert(s_GlobalState.listenForUnpairedDeviceActivity != 0, | |
| "This should only be called while listening for unpaired device activity"); | |
| if (s_GlobalState.listenForUnpairedDeviceActivity == 0) | |
| return; | |
| // Ignore input in editor. | |
| if (InputState.currentUpdateType == InputUpdateType.Editor) | |
| return; | |
| // Ignore any state change not triggered from a state event. | |
| var eventType = eventPtr.type; | |
| if (eventType != StateEvent.Type && eventType != DeltaStateEvent.Type) | |
| return; | |
| // Ignore event if device is disabled. | |
| if (!device.enabled) | |
| return; | |
| // See if it's a device not belonging to any user. | |
| if (ArrayHelpers.ContainsReference(s_GlobalState.allPairedDevices, s_GlobalState.allPairedDeviceCount, device)) | |
| { | |
| // No, it's a device already paired to a player so do nothing. | |
| return; | |
| } | |
| Profiler.BeginSample("InputCheckForUnpairedDeviceActivity"); | |
| // Apply the pre-filter. If there's callbacks and none of them return true, | |
| // we early out and ignore the event entirely. | |
| if (!DelegateHelpers.InvokeCallbacksSafe_AnyCallbackReturnsTrue( | |
| ref s_GlobalState.onPreFilterUnpairedDeviceUsed, device, eventPtr, "InputUser.onPreFilterUnpairedDeviceActivity")) | |
| return; | |
| // Go through the changed controls in the event and look for ones actuated | |
| // above a magnitude of a little above zero. | |
| foreach (var control in eventPtr.EnumerateChangedControls(device: device, magnitudeThreshold: 0.0001f)) | |
| { | |
| var deviceHasBeenPaired = false; | |
| s_GlobalState.onUnpairedDeviceUsed.LockForChanges(); | |
| for (var n = 0; n < s_GlobalState.onUnpairedDeviceUsed.length; ++n) | |
| { | |
| var pairingStateVersionBefore = s_GlobalState.pairingStateVersion; | |
| try | |
| { | |
| s_GlobalState.onUnpairedDeviceUsed[n](control, eventPtr); | |
| } | |
| catch (Exception exception) | |
| { | |
| Debug.LogError($"{exception.GetType().Name} while executing 'InputUser.onUnpairedDeviceUsed' callbacks"); | |
| Debug.LogException(exception); | |
| } | |
| if (pairingStateVersionBefore != s_GlobalState.pairingStateVersion | |
| && FindUserPairedToDevice(device) != null) | |
| { | |
| deviceHasBeenPaired = true; | |
| break; | |
| } | |
| } | |
| s_GlobalState.onUnpairedDeviceUsed.UnlockForChanges(); | |
| // If the device was paired in one of the callbacks, stop processing | |
| // changes on it. | |
| if (deviceHasBeenPaired) | |
| break; | |
| } | |
| Profiler.EndSample(); | |
| } | |
| /// <summary> | |
| /// Syntax for configuring a control scheme on a user. | |
| /// </summary> | |
| public struct ControlSchemeChangeSyntax | |
| { | |
| /// <summary> | |
| /// Leave the user's paired devices in place but pair any available devices | |
| /// that are still required by the control scheme. | |
| /// </summary> | |
| /// <returns></returns> | |
| /// <remarks> | |
| /// If there are unpaired devices that, at the platform level, are associated with the same | |
| /// user account, those will take precedence over other unpaired devices. | |
| /// </remarks> | |
| public ControlSchemeChangeSyntax AndPairRemainingDevices() | |
| { | |
| UpdateControlSchemeMatch(m_UserIndex, autoPairMissing: true); | |
| return this; | |
| } | |
| internal int m_UserIndex; | |
| } | |
| private uint m_Id; | |
| [] | |
| internal enum UserFlags | |
| { | |
| BindToAllDevices = 1 << 0, | |
| /// <summary> | |
| /// Whether we have initiated a user account selection. | |
| /// </summary> | |
| UserAccountSelectionInProgress = 1 << 1, | |
| } | |
| /// <summary> | |
| /// Data we store for each user. | |
| /// </summary> | |
| private struct UserData | |
| { | |
| /// <summary> | |
| /// The platform handle associated with the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// If set, this identifies the user on the platform. It also means that the devices | |
| /// assigned to the user may be paired at the platform level. | |
| /// </remarks> | |
| public InputUserAccountHandle? platformUserAccountHandle; | |
| /// <summary> | |
| /// Plain-text user name as returned by the underlying platform. Null if not associated with user on platform. | |
| /// </summary> | |
| public string platformUserAccountName; | |
| /// <summary> | |
| /// Platform-specific ID that identifies the user across sessions even if the user | |
| /// name changes. | |
| /// </summary> | |
| /// <remarks> | |
| /// This might not be a human-readable string. | |
| /// </remarks> | |
| public string platformUserAccountId; | |
| /// <summary> | |
| /// Number of devices in <see cref="InputUser.s_AllPairedDevices"/> assigned to the user. | |
| /// </summary> | |
| public int deviceCount; | |
| /// <summary> | |
| /// Index in <see cref="InputUser.s_AllPairedDevices"/> where the devices for this user start. Only valid | |
| /// if <see cref="deviceCount"/> is greater than zero. | |
| /// </summary> | |
| public int deviceStartIndex; | |
| /// <summary> | |
| /// Input actions associated with the user. | |
| /// </summary> | |
| public IInputActionCollection actions; | |
| /// <summary> | |
| /// Currently active control scheme or null if no control scheme has been set on the user. | |
| /// </summary> | |
| /// <remarks> | |
| /// This also dictates the binding mask that we're using with <see cref="actions"/>. | |
| /// </remarks> | |
| public InputControlScheme? controlScheme; | |
| public InputControlScheme.MatchResult controlSchemeMatch; | |
| /// <summary> | |
| /// Number of devices in <see cref="InputUser.s_AllLostDevices"/> assigned to the user. | |
| /// </summary> | |
| public int lostDeviceCount; | |
| /// <summary> | |
| /// Index in <see cref="InputUser.s_AllLostDevices"/> where the lost devices for this user start. Only valid | |
| /// if <see cref="lostDeviceCount"/> is greater than zero. | |
| /// </summary> | |
| public int lostDeviceStartIndex; | |
| ////TODO | |
| //public InputUserSettings settings; | |
| public UserFlags flags; | |
| } | |
| /// <summary> | |
| /// Compare two devices for being associated with a specific platform user account. | |
| /// </summary> | |
| private struct CompareDevicesByUserAccount : IComparer<InputDevice> | |
| { | |
| public InputUserAccountHandle platformUserAccountHandle; | |
| public int Compare(InputDevice x, InputDevice y) | |
| { | |
| var firstAccountHandle = GetUserAccountHandleForDevice(x); | |
| var secondAccountHandle = GetUserAccountHandleForDevice(x); | |
| if (firstAccountHandle == platformUserAccountHandle && | |
| secondAccountHandle == platformUserAccountHandle) | |
| return 0; | |
| if (firstAccountHandle == platformUserAccountHandle) | |
| return -1; | |
| if (secondAccountHandle == platformUserAccountHandle) | |
| return 1; | |
| return 0; | |
| } | |
| [] | |
| private static InputUserAccountHandle? GetUserAccountHandleForDevice(InputDevice device) | |
| { | |
| ////TODO (need to cache this) | |
| return null; | |
| } | |
| } | |
| private struct OngoingAccountSelection | |
| { | |
| public InputDevice device; | |
| public uint userId; | |
| } | |
| private struct GlobalState | |
| { | |
| internal int pairingStateVersion; | |
| internal uint lastUserId; | |
| internal int allUserCount; | |
| internal int allPairedDeviceCount; | |
| internal int allLostDeviceCount; | |
| internal InputUser[] allUsers; | |
| internal UserData[] allUserData; | |
| internal InputDevice[] allPairedDevices; // We keep a single array that we slice out to each user. | |
| internal InputDevice[] allLostDevices; // We keep a single array that we slice out to each user. | |
| internal InlinedArray<OngoingAccountSelection> ongoingAccountSelections; | |
| internal CallbackArray<Action<InputUser, InputUserChange, InputDevice>> onChange; | |
| internal CallbackArray<Action<InputControl, InputEventPtr>> onUnpairedDeviceUsed; | |
| internal CallbackArray<Func<InputDevice, InputEventPtr, bool>> onPreFilterUnpairedDeviceUsed; | |
| internal Action<object, InputActionChange> actionChangeDelegate; | |
| internal Action<InputDevice, InputDeviceChange> onDeviceChangeDelegate; | |
| internal Action<InputEventPtr, InputDevice> onEventDelegate; | |
| internal bool onActionChangeHooked; | |
| internal bool onDeviceChangeHooked; | |
| internal bool onEventHooked; | |
| internal int listenForUnpairedDeviceActivity; | |
| } | |
| private static GlobalState s_GlobalState; | |
| internal static ISavedState SaveAndResetState() | |
| { | |
| // Save current state and provide an opaque interface to restore it | |
| var savedState = new SavedStructState<GlobalState>( | |
| ref s_GlobalState, | |
| (ref GlobalState state) => s_GlobalState = state, // restore | |
| () => DisposeAndResetGlobalState()); // static dispose | |
| // Reset global state | |
| s_GlobalState = default; | |
| return savedState; | |
| } | |
| private static void HookIntoActionChange() | |
| { | |
| if (s_GlobalState.onActionChangeHooked) | |
| return; | |
| if (s_GlobalState.actionChangeDelegate == null) | |
| s_GlobalState.actionChangeDelegate = OnActionChange; | |
| InputSystem.onActionChange += OnActionChange; | |
| s_GlobalState.onActionChangeHooked = true; | |
| } | |
| private static void UnhookFromActionChange() | |
| { | |
| if (!s_GlobalState.onActionChangeHooked) | |
| return; | |
| InputSystem.onActionChange -= OnActionChange; | |
| s_GlobalState.onActionChangeHooked = false; | |
| } | |
| private static void HookIntoDeviceChange() | |
| { | |
| if (s_GlobalState.onDeviceChangeHooked) | |
| return; | |
| if (s_GlobalState.onDeviceChangeDelegate == null) | |
| s_GlobalState.onDeviceChangeDelegate = OnDeviceChange; | |
| InputSystem.onDeviceChange += s_GlobalState.onDeviceChangeDelegate; | |
| s_GlobalState.onDeviceChangeHooked = true; | |
| } | |
| private static void UnhookFromDeviceChange() | |
| { | |
| if (!s_GlobalState.onDeviceChangeHooked) | |
| return; | |
| InputSystem.onDeviceChange -= s_GlobalState.onDeviceChangeDelegate; | |
| s_GlobalState.onDeviceChangeHooked = false; | |
| } | |
| private static void HookIntoEvents() | |
| { | |
| if (s_GlobalState.onEventHooked) | |
| return; | |
| if (s_GlobalState.onEventDelegate == null) | |
| s_GlobalState.onEventDelegate = OnEvent; | |
| InputSystem.onEvent += s_GlobalState.onEventDelegate; | |
| s_GlobalState.onEventHooked = true; | |
| } | |
| private static void UnhookFromDeviceStateChange() | |
| { | |
| if (!s_GlobalState.onEventHooked) | |
| return; | |
| InputSystem.onEvent -= s_GlobalState.onEventDelegate; | |
| s_GlobalState.onEventHooked = false; | |
| } | |
| private static void DisposeAndResetGlobalState() | |
| { | |
| // Release native memory held by control scheme match results. | |
| for (var i = 0; i < s_GlobalState.allUserCount; ++i) | |
| s_GlobalState.allUserData[i].controlSchemeMatch.Dispose(); | |
| // Don't reset s_LastUserId and just let it increment instead so we never generate | |
| // the same ID twice. | |
| var storedLastUserId = s_GlobalState.lastUserId; | |
| s_GlobalState = default; | |
| s_GlobalState.lastUserId = storedLastUserId; | |
| } | |
| internal static void ResetGlobals() | |
| { | |
| UnhookFromActionChange(); | |
| UnhookFromDeviceChange(); | |
| UnhookFromDeviceStateChange(); | |
| DisposeAndResetGlobalState(); | |
| } | |
| } | |
| } | |