A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections; 3using System.Collections.Generic; 4using System.Linq; 5using System.Text; 6using Unity.Collections; 7using UnityEngine.InputSystem.Layouts; 8using UnityEngine.InputSystem.Utilities; 9#if UNITY_EDITOR 10using UnityEditor; 11#endif 12 13////TODO: introduce the concept of a "variation" 14//// - a variation is just a variant of a control scheme, not a full control scheme by itself 15//// - an individual variation can be toggled on and off independently 16//// - while a control is is active, all its variations that are toggled on are also active 17//// - assignment to variations works the same as assignment to control schemes 18//// use case: left/right stick toggles, left/right bumper toggles, etc 19 20////TODO: introduce concept of precedence where one control scheme will be preferred over another that is also a match 21//// (might be its enough to represent this simply through ordering by giving the user control over the ordering through the UI) 22 23////REVIEW: allow associating control schemes with platforms, too? 24 25namespace UnityEngine.InputSystem 26{ 27 /// <summary> 28 /// A named set of zero or more device requirements along with an associated binding group. 29 /// </summary> 30 /// <remarks> 31 /// Control schemes provide an additional layer on top of binding groups. While binding 32 /// groups allow differentiating sets of bindings (e.g. a "Keyboard&amp;Mouse" group versus 33 /// a "Gamepad" group), control schemes impose a set of devices requirements that must be 34 /// met in order for a specific set of bindings to be usable. 35 /// 36 /// Note that control schemes can only be defined at the <see cref="InputActionAsset"/> level. 37 /// </remarks> 38 /// <seealso cref="InputActionAsset.controlSchemes"/> 39 /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> 40 [Serializable] 41 public struct InputControlScheme : IEquatable<InputControlScheme> 42 { 43 /// <summary> 44 /// Name of the control scheme. Not <c>null</c> or empty except if InputControlScheme 45 /// instance is invalid (i.e. default-initialized). 46 /// </summary> 47 /// <value>Name of the scheme.</value> 48 /// <remarks> 49 /// May be empty or null except if the control scheme is part of an <see cref="InputActionAsset"/>. 50 /// </remarks> 51 /// <seealso cref="InputActionSetupExtensions.AddControlScheme(InputActionAsset,string)"/> 52 public string name => m_Name; 53 54 /// <summary> 55 /// Binding group that is associated with the control scheme. Not <c>null</c> or empty 56 /// except if InputControlScheme is invalid (i.e. default-initialized). 57 /// </summary> 58 /// <value>Binding group for the scheme.</value> 59 /// <remarks> 60 /// All bindings in this group are considered to be part of the control scheme. 61 /// </remarks> 62 /// <seealso cref="InputBinding.groups"/> 63 public string bindingGroup 64 { 65 get => m_BindingGroup; 66 set => m_BindingGroup = value; 67 } 68 69 /// <summary> 70 /// Devices used by the control scheme. 71 /// </summary> 72 /// <value>Device requirements of the scheme.</value> 73 /// <remarks> 74 /// No two entries will be allowed to match the same control or device at runtime in order for the requirements 75 /// of the control scheme to be considered satisfied. If, for example, one entry requires a "&lt;Gamepad&gt;" and 76 /// another entry requires a "&lt;Gamepad&gt;", then at runtime two gamepads will be required even though a single 77 /// one will match both requirements individually. However, if, for example, one entry requires "&lt;Gamepad&gt;/leftStick" 78 /// and another requires "&lt;Gamepad&gt;, the same device can match both requirements as each one resolves to 79 /// a different control. 80 /// 81 /// It it allowed to define control schemes without device requirements, i.e. for which this 82 /// property will be an empty array. Note, however, that features such as automatic control scheme 83 /// switching in <see cref="PlayerInput"/> will not work with such control schemes. 84 /// </remarks> 85 public ReadOnlyArray<DeviceRequirement> deviceRequirements => 86 new ReadOnlyArray<DeviceRequirement>(m_DeviceRequirements); 87 88 /// <summary> 89 /// Initialize the control scheme with the given name, device requirements, 90 /// and binding group. 91 /// </summary> 92 /// <param name="name">Name to use for the scheme. Required.</param> 93 /// <param name="devices">List of device requirements.</param> 94 /// <param name="bindingGroup">Name to use for the binding group (see <see cref="InputBinding.groups"/>) 95 /// associated with the control scheme. If this is <c>null</c> or empty, <paramref name="name"/> is 96 /// used instead (with <see cref="InputBinding.Separator"/> characters stripped from the name).</param> 97 /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c> or empty.</exception> 98 public InputControlScheme(string name, IEnumerable<DeviceRequirement> devices = null, string bindingGroup = null) 99 : this() 100 { 101 if (string.IsNullOrEmpty(name)) 102 throw new ArgumentNullException(nameof(name)); 103 104 SetNameAndBindingGroup(name, bindingGroup); 105 106 m_DeviceRequirements = null; 107 if (devices != null) 108 { 109 m_DeviceRequirements = devices.ToArray(); 110 if (m_DeviceRequirements.Length == 0) 111 m_DeviceRequirements = null; 112 } 113 } 114 115 #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 116 internal InputControlScheme(SerializedProperty sp) 117 { 118 var requirements = new List<DeviceRequirement>(); 119 var deviceRequirementsArray = sp.FindPropertyRelative(nameof(m_DeviceRequirements)); 120 if (deviceRequirementsArray == null) 121 throw new ArgumentException("The serialized property does not contain an InputControlScheme object."); 122 123 foreach (SerializedProperty deviceRequirement in deviceRequirementsArray) 124 { 125 requirements.Add(new DeviceRequirement 126 { 127 controlPath = deviceRequirement.FindPropertyRelative(nameof(DeviceRequirement.m_ControlPath)).stringValue, 128 m_Flags = (DeviceRequirement.Flags)deviceRequirement.FindPropertyRelative(nameof(DeviceRequirement.m_Flags)).enumValueFlag 129 }); 130 } 131 132 m_Name = sp.FindPropertyRelative(nameof(m_Name)).stringValue; 133 m_DeviceRequirements = requirements.ToArray(); 134 m_BindingGroup = sp.FindPropertyRelative(nameof(m_BindingGroup)).stringValue; 135 } 136 137 #endif 138 139 internal void SetNameAndBindingGroup(string name, string bindingGroup = null) 140 { 141 m_Name = name; 142 if (!string.IsNullOrEmpty(bindingGroup)) 143 m_BindingGroup = bindingGroup; 144 else 145 m_BindingGroup = name.Contains(InputBinding.Separator) 146 ? name.Replace(InputBinding.kSeparatorString, "") 147 : name; 148 } 149 150 /// <summary> 151 /// Given a list of devices and a list of control schemes, find the most suitable control 152 /// scheme to use with the devices. 153 /// </summary> 154 /// <param name="devices">A list of devices. If the list is empty, only schemes with 155 /// empty <see cref="deviceRequirements"/> lists will get matched.</param> 156 /// <param name="schemes">A list of control schemes.</param> 157 /// <param name="mustIncludeDevice">If not <c>null</c>, a successful match has to include the given device.</param> 158 /// <param name="allowUnsuccesfulMatch">If true, then allow returning a match that has unsatisfied requirements but still 159 /// matched at least some requirement. If there are several unsuccessful matches, the returned scheme is still the highest 160 /// scoring one among those.</param> 161 /// <typeparam name="TDevices">Collection type to use for the list of devices.</typeparam> 162 /// <typeparam name="TSchemes">Collection type to use for the list of schemes.</typeparam> 163 /// <returns>The control scheme that best matched the given devices or <c>null</c> if no 164 /// scheme was found suitable.</returns> 165 /// <exception cref="ArgumentNullException"><paramref name="devices"/> is <c>null</c> -or- 166 /// <paramref name="schemes"/> is <c>null</c>.</exception> 167 /// <remarks> 168 /// Any successful match (see <see cref="MatchResult.isSuccessfulMatch"/>) will be considered. 169 /// The one that matches the most amount of devices (see <see cref="MatchResult.devices"/>) 170 /// will be returned. If more than one schemes matches equally well, the first one encountered 171 /// in the list is returned. 172 /// 173 /// Note that schemes are not required to match all devices available in the list. The result 174 /// will simply be the scheme that matched the most devices of what was devices. Use <see 175 /// cref="PickDevicesFrom{TDevices}"/> to find the devices that a control scheme selects. 176 /// 177 /// This method is parameterized over <typeparamref name="TDevices"/> and <typeparamref name="TSchemes"/> 178 /// to allow avoiding GC heap allocations from boxing of structs such as <see cref="ReadOnlyArray{TValue}"/>. 179 /// 180 /// <example> 181 /// <code> 182 /// // Create an .inputactions asset. 183 /// var asset = ScriptableObject.CreateInstance&lt;InputActionAsset&gt;(); 184 /// 185 /// // Add some control schemes to the asset. 186 /// asset.AddControlScheme("KeyboardMouse") 187 /// .WithRequiredDevice&lt;Keyboard&gt;() 188 /// .WithRequiredDevice&lt;Mouse&gt;()); 189 /// asset.AddControlScheme("Gamepad") 190 /// .WithRequiredDevice&lt;Gamepad&gt;()); 191 /// asset.AddControlScheme("DualGamepad") 192 /// .WithRequiredDevice&lt;Gamepad&gt;()) 193 /// .WithOptionalGamepad&lt;Gamepad&gt;()); 194 /// 195 /// // Add some devices that we can test with. 196 /// var keyboard = InputSystem.AddDevice&lt;Keyboard&gt;(); 197 /// var mouse = InputSystem.AddDevice&lt;Mouse&gt;(); 198 /// var gamepad1 = InputSystem.AddDevice&lt;Gamepad&gt;(); 199 /// var gamepad2 = InputSystem.AddDevice&lt;Gamepad&gt;(); 200 /// 201 /// // Matching with just a keyboard won't match any scheme. 202 /// InputControlScheme.FindControlSchemeForDevices( 203 /// new InputDevice[] { keyboard }, asset.controlSchemes); 204 /// 205 /// // Matching with a keyboard and mouse with match the "KeyboardMouse" scheme. 206 /// InputControlScheme.FindControlSchemeForDevices( 207 /// new InputDevice[] { keyboard, mouse }, asset.controlSchemes); 208 /// 209 /// // Matching with a single gamepad will match the "Gamepad" scheme. 210 /// // Note that since the second gamepad is optional in "DualGamepad" could 211 /// // match the same set of devices but it doesn't match any better than 212 /// // "Gamepad" and that one comes first in the list. 213 /// InputControlScheme.FindControlSchemeForDevices( 214 /// new InputDevice[] { gamepad1 }, asset.controlSchemes); 215 /// 216 /// // Matching with two gamepads will match the "DualGamepad" scheme. 217 /// // Note that "Gamepad" will match this device list as well. If "DualGamepad" 218 /// // didn't exist, "Gamepad" would be the result here. However, "DualGamepad" 219 /// // matches the list better than "Gamepad" so that's what gets returned here. 220 /// InputControlScheme.FindControlSchemeForDevices( 221 /// new InputDevice[] { gamepad1, gamepad2 }, asset.controlSchemes); 222 /// </code> 223 /// </example> 224 /// </remarks> 225 public static InputControlScheme? FindControlSchemeForDevices<TDevices, TSchemes>(TDevices devices, TSchemes schemes, InputDevice mustIncludeDevice = null, bool allowUnsuccesfulMatch = false) 226 where TDevices : IReadOnlyList<InputDevice> 227 where TSchemes : IEnumerable<InputControlScheme> 228 { 229 if (devices == null) 230 throw new ArgumentNullException(nameof(devices)); 231 if (schemes == null) 232 throw new ArgumentNullException(nameof(schemes)); 233 234 if (!FindControlSchemeForDevices(devices, schemes, out var controlScheme, out var matchResult, mustIncludeDevice, allowUnsuccesfulMatch)) 235 return null; 236 237 matchResult.Dispose(); 238 return controlScheme; 239 } 240 241 public static bool FindControlSchemeForDevices<TDevices, TSchemes>(TDevices devices, TSchemes schemes, 242 out InputControlScheme controlScheme, out MatchResult matchResult, InputDevice mustIncludeDevice = null, bool allowUnsuccessfulMatch = false) 243 where TDevices : IReadOnlyList<InputDevice> 244 where TSchemes : IEnumerable<InputControlScheme> 245 { 246 if (devices == null) 247 throw new ArgumentNullException(nameof(devices)); 248 if (schemes == null) 249 throw new ArgumentNullException(nameof(schemes)); 250 251 MatchResult? bestResult = null; 252 InputControlScheme? bestScheme = null; 253 254 foreach (var scheme in schemes) 255 { 256 var result = scheme.PickDevicesFrom(devices, favorDevice: mustIncludeDevice); 257 258 // Ignore if scheme doesn't fit devices. 259 if (!result.isSuccessfulMatch && (!allowUnsuccessfulMatch || result.score <= 0)) 260 { 261 result.Dispose(); 262 continue; 263 } 264 265 // Ignore if we have a device we specifically want to be part of the result and 266 // the current match doesn't have it. 267 if (mustIncludeDevice != null && !result.devices.Contains(mustIncludeDevice)) 268 { 269 result.Dispose(); 270 continue; 271 } 272 273 // Ignore if it does fit but we already have a better fit. 274 if (bestResult != null && bestResult.Value.score >= result.score) 275 { 276 result.Dispose(); 277 continue; 278 } 279 280 bestResult?.Dispose(); 281 282 bestResult = result; 283 bestScheme = scheme; 284 } 285 286 matchResult = bestResult ?? default; 287 controlScheme = bestScheme ?? default; 288 289 return bestResult.HasValue; 290 } 291 292 ////FIXME: docs are wrong now 293 /// <summary> 294 /// Return the first control schemes from the given list that supports the given 295 /// device (see <see cref="SupportsDevice"/>). 296 /// </summary> 297 /// <param name="device">An input device.</param> 298 /// <param name="schemes">A list of control schemes. Can be empty.</param> 299 /// <typeparam name="TSchemes">Collection type to use for the list of schemes.</typeparam> 300 /// <returns>The first schemes from <paramref name="schemes"/> that supports <paramref name="device"/> 301 /// or <c>null</c> if none of the schemes is usable with the device.</returns> 302 /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c> -or- 303 /// <paramref name="schemes"/> is <c>null</c>.</exception> 304 public static InputControlScheme? FindControlSchemeForDevice<TSchemes>(InputDevice device, TSchemes schemes) 305 where TSchemes : IEnumerable<InputControlScheme> 306 { 307 if (schemes == null) 308 throw new ArgumentNullException(nameof(schemes)); 309 if (device == null) 310 throw new ArgumentNullException(nameof(device)); 311 312 return FindControlSchemeForDevices(new OneOrMore<InputDevice, ReadOnlyArray<InputDevice>>(device), schemes); 313 } 314 315 /// <summary> 316 /// Whether the control scheme has a requirement in <see cref="deviceRequirements"/> that 317 /// targets the given device. 318 /// </summary> 319 /// <param name="device">An input device.</param> 320 /// <returns>True if the control scheme has a device requirement matching the device.</returns> 321 /// <exception cref="ArgumentNullException"><paramref name="device"/> is <c>null</c>.</exception> 322 /// <remarks> 323 /// Note that both optional (see <see cref="DeviceRequirement.isOptional"/>) and non-optional 324 /// device requirements are taken into account. 325 /// 326 /// </remarks> 327 public bool SupportsDevice(InputDevice device) 328 { 329 if (device == null) 330 throw new ArgumentNullException(nameof(device)); 331 332 ////REVIEW: does this need to take AND and OR into account? 333 for (var i = 0; i < m_DeviceRequirements.Length; ++i) 334 { 335 var control = InputControlPath.TryFindControl(device, m_DeviceRequirements[i].controlPath); 336 if (control != null) 337 return true; 338 } 339 340 return false; 341 } 342 343 ////REVIEW: have mode where instead of matching only the first device that matches a requirement, we match as many 344 //// as we can get? (could be useful for single-player) 345 /// <summary> 346 /// Based on a list of devices, make a selection that matches the <see cref="deviceRequirements">requirements</see> 347 /// imposed by the control scheme. 348 /// </summary> 349 /// <param name="devices">A list of devices to choose from.</param> 350 /// <param name="favorDevice">If not null, the device will be favored over other devices in <paramref name="devices"/>. 351 /// Note that the device must be present in the list also.</param> 352 /// <returns>A <see cref="MatchResult"/> structure containing the result of the pick. Note that this structure 353 /// must be manually <see cref="MatchResult.Dispose">disposed</see> or unmanaged memory will be leaked.</returns> 354 /// <remarks> 355 /// Does not allocate managed memory. 356 /// </remarks> 357 public MatchResult PickDevicesFrom<TDevices>(TDevices devices, InputDevice favorDevice = null) 358 where TDevices : IReadOnlyList<InputDevice> 359 { 360 // Empty device requirements match anything while not really picking anything. 361 if (m_DeviceRequirements == null || m_DeviceRequirements.Length == 0) 362 { 363 return new MatchResult 364 { 365 m_Result = MatchResult.Result.AllSatisfied, 366 // Prevent zero score on successful match but make less than one which would 367 // result from having a single requirement. 368 m_Score = 0.5f, 369 }; 370 } 371 372 // Go through each requirement and match it. 373 // NOTE: Even if `devices` is empty, we don't know yet whether we have a NoMatch. 374 // All our devices may be optional. 375 var haveAllRequired = true; 376 var haveAllOptional = true; 377 var requirementCount = m_DeviceRequirements.Length; 378 var score = 0f; 379 var controls = new InputControlList<InputControl>(Allocator.Persistent, requirementCount); 380 try 381 { 382 var orChainIsSatisfied = false; 383 var orChainHasRequiredDevices = false; 384 for (var i = 0; i < requirementCount; ++i) 385 { 386 var isOR = m_DeviceRequirements[i].isOR; 387 var isOptional = m_DeviceRequirements[i].isOptional; 388 389 // If this is an OR requirement and we already have a match in this OR chain, 390 // skip this requirement. 391 if (isOR && orChainIsSatisfied) 392 { 393 // Skill need to add an entry for this requirement. 394 controls.Add(null); 395 continue; 396 } 397 398 // Null and empty paths shouldn't make it into the list but make double 399 // sure here. Simply ignore entries that don't have a path. 400 var path = m_DeviceRequirements[i].controlPath; 401 if (string.IsNullOrEmpty(path)) 402 { 403 score += 1; 404 controls.Add(null); 405 continue; 406 } 407 408 // Find the first matching control among the devices we have. 409 InputControl match = null; 410 for (var n = 0; n < devices.Count; ++n) 411 { 412 var device = devices[n]; 413 414 // If we should favor a device, we swap it in at index #0 regardless 415 // of where in the list the device occurs (it MUST, however, occur in the list). 416 if (favorDevice != null) 417 { 418 if (n == 0) 419 device = favorDevice; 420 else if (device == favorDevice) 421 device = devices[0]; 422 } 423 424 // See if we have a match. 425 var matchedControl = InputControlPath.TryFindControl(device, path); 426 if (matchedControl == null) 427 continue; // No. 428 429 // We have a match but if we've already matched the same control through another requirement, 430 // we can't use the match. 431 if (controls.Contains(matchedControl)) 432 continue; 433 434 match = matchedControl; 435 436 // Compute score for match. 437 var deviceLayoutOfControlPath = new InternedString(InputControlPath.TryGetDeviceLayout(path)); 438 if (deviceLayoutOfControlPath.IsEmpty()) 439 { 440 // Generic match adds 1 to score. 441 score += 1; 442 } 443 else 444 { 445 var deviceLayoutOfControl = matchedControl.device.m_Layout; 446 if (InputControlLayout.s_Layouts.ComputeDistanceInInheritanceHierarchy(deviceLayoutOfControlPath, 447 deviceLayoutOfControl, out var distance)) 448 { 449 score += 1 + 1f / (Math.Abs(distance) + 1); 450 } 451 else 452 { 453 // Shouldn't really get here as for the control to be a match for the path, the device layouts 454 // would be expected to be related to each other. But just add 1 for a generic match and go on. 455 score += 1; 456 } 457 } 458 459 break; 460 } 461 462 // Check requirements in AND and OR chains. We look ahead here to find out whether 463 // the next requirement is starting an OR chain. As the OR combines with the previous 464 // requirement in the list, this affects our current requirement. 465 var nextIsOR = i + 1 < requirementCount && m_DeviceRequirements[i + 1].isOR; 466 if (nextIsOR) 467 { 468 // Shouldn't get here if the chain is already satisfied. Should be handled 469 // at beginning of loop and we shouldn't even be looking at finding controls 470 // in that case. 471 Debug.Assert(!orChainIsSatisfied); 472 473 // It's an OR with the next requirement. Depends on the outcome of other matches whether 474 // we're good or not. 475 476 if (match != null) 477 { 478 // First match in this chain. 479 orChainIsSatisfied = true; 480 } 481 else 482 { 483 // Chain not satisfied yet. 484 485 if (!isOptional) 486 orChainHasRequiredDevices = true; 487 } 488 } 489 else if (isOR && i == requirementCount - 1) 490 { 491 // It's an OR at the very end of the requirements list. Terminate 492 // the OR chain. 493 494 if (match == null) 495 { 496 if (orChainHasRequiredDevices) 497 haveAllRequired = false; 498 else 499 haveAllOptional = false; 500 } 501 } 502 else 503 { 504 // It's an AND. 505 506 if (match == null) 507 { 508 if (isOptional) 509 haveAllOptional = false; 510 else 511 haveAllRequired = false; 512 } 513 514 // Terminate ongoing OR chain. 515 if (i > 0 && m_DeviceRequirements[i - 1].isOR) 516 { 517 if (!orChainIsSatisfied) 518 { 519 if (orChainHasRequiredDevices) 520 haveAllRequired = false; 521 else 522 haveAllOptional = false; 523 } 524 orChainIsSatisfied = false; 525 } 526 } 527 528 // Add match to list. Maybe null. 529 controls.Add(match); 530 } 531 532 // We should have matched each of our requirements. 533 Debug.Assert(controls.Count == requirementCount); 534 } 535 catch (Exception) 536 { 537 controls.Dispose(); 538 throw; 539 } 540 541 return new MatchResult 542 { 543 m_Result = !haveAllRequired 544 ? MatchResult.Result.MissingRequired 545 : !haveAllOptional 546 ? MatchResult.Result.MissingOptional 547 : MatchResult.Result.AllSatisfied, 548 m_Controls = controls, 549 m_Requirements = m_DeviceRequirements, 550 m_Score = score, 551 }; 552 } 553 554 public bool Equals(InputControlScheme other) 555 { 556 if (!(string.Equals(m_Name, other.m_Name, StringComparison.InvariantCultureIgnoreCase) && 557 string.Equals(m_BindingGroup, other.m_BindingGroup, StringComparison.InvariantCultureIgnoreCase))) 558 return false; 559 560 // Compare device requirements. 561 if (m_DeviceRequirements == null || m_DeviceRequirements.Length == 0) 562 return other.m_DeviceRequirements == null || other.m_DeviceRequirements.Length == 0; 563 if (other.m_DeviceRequirements == null || m_DeviceRequirements.Length != other.m_DeviceRequirements.Length) 564 return false; 565 566 var deviceCount = m_DeviceRequirements.Length; 567 for (var i = 0; i < deviceCount; ++i) 568 { 569 var device = m_DeviceRequirements[i]; 570 var haveMatch = false; 571 for (var n = 0; n < deviceCount; ++n) 572 { 573 if (other.m_DeviceRequirements[n] == device) 574 { 575 haveMatch = true; 576 break; 577 } 578 } 579 580 if (!haveMatch) 581 return false; 582 } 583 584 return true; 585 } 586 587 public override bool Equals(object obj) 588 { 589 if (ReferenceEquals(null, obj)) 590 return false; 591 592 return obj is InputControlScheme && Equals((InputControlScheme)obj); 593 } 594 595 public override int GetHashCode() 596 { 597 unchecked 598 { 599 var hashCode = (m_Name != null ? m_Name.GetHashCode() : 0); 600 hashCode = (hashCode * 397) ^ (m_BindingGroup != null ? m_BindingGroup.GetHashCode() : 0); 601 hashCode = (hashCode * 397) ^ (m_DeviceRequirements != null ? m_DeviceRequirements.GetHashCode() : 0); 602 return hashCode; 603 } 604 } 605 606 public override string ToString() 607 { 608 if (string.IsNullOrEmpty(m_Name)) 609 return base.ToString(); 610 611 if (m_DeviceRequirements == null) 612 return m_Name; 613 614 var builder = new StringBuilder(); 615 builder.Append(m_Name); 616 builder.Append('('); 617 618 var isFirst = true; 619 foreach (var device in m_DeviceRequirements) 620 { 621 if (!isFirst) 622 builder.Append(','); 623 624 builder.Append(device.controlPath); 625 isFirst = false; 626 } 627 628 builder.Append(')'); 629 return builder.ToString(); 630 } 631 632 public static bool operator==(InputControlScheme left, InputControlScheme right) 633 { 634 return left.Equals(right); 635 } 636 637 public static bool operator!=(InputControlScheme left, InputControlScheme right) 638 { 639 return !left.Equals(right); 640 } 641 642 [SerializeField] internal string m_Name; 643 [SerializeField] internal string m_BindingGroup; 644 [SerializeField] internal DeviceRequirement[] m_DeviceRequirements; 645 646 /// <summary> 647 /// The result of matching a list of <see cref="InputDevice">devices</see> against a list of 648 /// <see cref="DeviceRequirement">requirements</see> in an <see cref="InputControlScheme"/>. 649 /// </summary> 650 /// <remarks> 651 /// This struct uses <see cref="InputControlList{TControl}"/> which allocates unmanaged memory 652 /// and thus must be disposed in order to not leak unmanaged heap memory. 653 /// </remarks> 654 /// <seealso cref="InputControlScheme.PickDevicesFrom{TDevices}"/> 655 public struct MatchResult : IEnumerable<MatchResult.Match>, IDisposable 656 { 657 /// <summary> 658 /// Overall, relative measure for how well the control scheme matches. 659 /// </summary> 660 /// <value>Scoring value for the control scheme match.</value> 661 /// <remarks> 662 /// Two control schemes may, for example, both support gamepads but one may be tailored to a specific 663 /// gamepad whereas the other one is a generic gamepad control scheme. To differentiate the two, we need 664 /// to know not only that a control schemes but how well it matches relative to other schemes. This is 665 /// what the score value is used for. 666 /// 667 /// Scores are computed primarily based on layouts referenced from device requirements. To start with, each 668 /// matching device requirement (whether optional or mandatory) will add 1 to the score. This the base 669 /// score of a match. Then, for each requirement a delta is computed from the device layout referenced by 670 /// the requirement to the device layout used by the matching control. For example, if the requirement is 671 /// <c>"&lt;Gamepad&gt;</c> and the matching control uses the <see cref="DualShock.DualShock4GamepadHID"/> 672 /// layout, the delta is 2 as the latter layout is derived from <see cref="Gamepad"/> via the intermediate 673 /// <see cref="DualShock.DualShockGamepad"/> layout, i.e. two steps in the inheritance hierarchy. The 674 /// <em>inverse</em> of the delta plus one, i.e. <c>1/(delta+1)</c> is then added to the score. This means 675 /// that an exact match will add an additional 1 to the score and less exact matches will add progressively 676 /// smaller values to the score (proportional to the distance of the actual layout to the one used in the 677 /// requirement). 678 /// 679 /// What this leads to is that, for example, a control scheme with a <c>"&lt;Gamepad&gt;"</c> requirement 680 /// will match a <see cref="DualShock.DualShock4GamepadHID"/> with a <em>lower</em> score than a control 681 /// scheme with a <c>"&lt;DualShockGamepad&gt;"</c> requirement as the <see cref="Gamepad"/> layout is 682 /// further removed (i.e. smaller inverse delta) from <see cref="DualShock.DualShock4GamepadHID"/> than 683 /// <see cref="DualShock.DualShockGamepad"/>. 684 /// </remarks> 685 public float score => m_Score; 686 687 /// <summary> 688 /// Whether the device requirements got successfully matched. 689 /// </summary> 690 /// <value>True if the scheme's device requirements were satisfied.</value> 691 public bool isSuccessfulMatch => m_Result != Result.MissingRequired; 692 693 /// <summary> 694 /// Whether there are missing required devices. 695 /// </summary> 696 /// <value>True if there are missing, non-optional devices.</value> 697 /// <seealso cref="DeviceRequirement.isOptional"/> 698 public bool hasMissingRequiredDevices => m_Result == Result.MissingRequired; 699 700 /// <summary> 701 /// Whether there are missing optional devices. This does not prevent 702 /// a successful match. 703 /// </summary> 704 /// <value>True if there are missing optional devices.</value> 705 /// <seealso cref="DeviceRequirement.isOptional"/> 706 public bool hasMissingOptionalDevices => m_Result == Result.MissingOptional; 707 708 /// <summary> 709 /// The devices that got picked from the available devices. 710 /// </summary> 711 public InputControlList<InputDevice> devices 712 { 713 get 714 { 715 // Lazily construct the device list. If we have missing required 716 // devices, though, always return an empty list. The user can still see 717 // the individual matches on each of the requirement entries but we 718 // consider the device picking itself failed. 719 if (m_Devices.Count == 0 && !hasMissingRequiredDevices) 720 { 721 var controlCount = m_Controls.Count; 722 if (controlCount != 0) 723 { 724 m_Devices.Capacity = controlCount; 725 for (var i = 0; i < controlCount; ++i) 726 { 727 var control = m_Controls[i]; 728 if (control == null) 729 continue; 730 731 var device = control.device; 732 if (m_Devices.Contains(device)) 733 continue; // Duplicate match of same device. 734 735 m_Devices.Add(device); 736 } 737 } 738 } 739 740 return m_Devices; 741 } 742 } 743 744 public Match this[int index] 745 { 746 get 747 { 748 if (index < 0 || m_Requirements == null || index >= m_Requirements.Length) 749 throw new ArgumentOutOfRangeException("index"); 750 return new Match 751 { 752 m_RequirementIndex = index, 753 m_Requirements = m_Requirements, 754 m_Controls = m_Controls, 755 }; 756 } 757 } 758 759 /// <summary> 760 /// Enumerate the match for each individual <see cref="DeviceRequirement"/> in the control scheme. 761 /// </summary> 762 /// <returns>An enumerate going over each individual match.</returns> 763 public IEnumerator<Match> GetEnumerator() 764 { 765 return new Enumerator 766 { 767 m_Index = -1, 768 m_Requirements = m_Requirements, 769 m_Controls = m_Controls, 770 }; 771 } 772 773 /// <summary> 774 /// Enumerate the match for each individual <see cref="DeviceRequirement"/> in the control scheme. 775 /// </summary> 776 /// <returns>An enumerate going over each individual match.</returns> 777 IEnumerator IEnumerable.GetEnumerator() 778 { 779 return GetEnumerator(); 780 } 781 782 /// <summary> 783 /// Discard the list of devices. 784 /// </summary> 785 public void Dispose() 786 { 787 m_Controls.Dispose(); 788 m_Devices.Dispose(); 789 } 790 791 internal Result m_Result; 792 internal float m_Score; 793 internal InputControlList<InputDevice> m_Devices; 794 internal InputControlList<InputControl> m_Controls; 795 internal DeviceRequirement[] m_Requirements; 796 797 internal enum Result 798 { 799 AllSatisfied, 800 MissingRequired, 801 MissingOptional, 802 } 803 804 ////REVIEW: would be great to not have to repeatedly copy InputControlLists around 805 806 /// <summary> 807 /// A single matched <see cref="DeviceRequirement"/>. 808 /// </summary> 809 /// <remarks> 810 /// Links the control that was matched with the respective device requirement. 811 /// </remarks> 812 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "Conflicts with UnityEngine.Networking.Match, which is deprecated and will go away.")] 813 public struct Match 814 { 815 /// <summary> 816 /// The control that was match from the requirement's <see cref="DeviceRequirement.controlPath"/> 817 /// </summary> 818 /// <remarks> 819 /// This is the same as <see cref="device"/> if the <see cref="DeviceRequirement.controlPath">control 820 /// path</see> matches the device directly rather than matching a control on the device. 821 /// 822 /// Note that while a control path can match arbitrary many controls, only the first matched control 823 /// will be returned here. To get all controls that were matched by a specific requirement, a 824 /// manual query must be performed using <see cref="InputControlPath"/>. 825 /// 826 /// If the match failed, this will be null. 827 /// </remarks> 828 public InputControl control => m_Controls[m_RequirementIndex]; 829 830 /// <summary> 831 /// The device that got matched. 832 /// </summary> 833 /// <remarks> 834 /// If a specific control on the device was matched, this will be <see cref="InputControl.device"/> or 835 /// <see cref="control"/>. If a device was matched directly, this will be the same as <see cref="control"/>. 836 /// </remarks> 837 public InputDevice device 838 { 839 get 840 { 841 var control = this.control; 842 return control?.device; 843 } 844 } 845 846 /// <summary> 847 /// Index of the requirement in <see cref="InputControlScheme.deviceRequirements"/>. 848 /// </summary> 849 public int requirementIndex => m_RequirementIndex; 850 851 /// <summary> 852 /// The device requirement that got matched. 853 /// </summary> 854 public DeviceRequirement requirement => m_Requirements[m_RequirementIndex]; 855 856 public bool isOptional => requirement.isOptional; 857 858 internal int m_RequirementIndex; 859 internal DeviceRequirement[] m_Requirements; 860 internal InputControlList<InputControl> m_Controls; 861 } 862 863 private struct Enumerator : IEnumerator<Match> 864 { 865 public bool MoveNext() 866 { 867 ++m_Index; 868 return m_Requirements != null && m_Index < m_Requirements.Length; 869 } 870 871 public void Reset() 872 { 873 m_Index = -1; 874 } 875 876 public Match Current 877 { 878 get 879 { 880 if (m_Requirements == null || m_Index < 0 || m_Index >= m_Requirements.Length) 881 throw new InvalidOperationException("Enumerator is not valid"); 882 883 return new Match 884 { 885 m_RequirementIndex = m_Index, 886 m_Requirements = m_Requirements, 887 m_Controls = m_Controls, 888 }; 889 } 890 } 891 892 object IEnumerator.Current => Current; 893 894 public void Dispose() 895 { 896 } 897 898 internal int m_Index; 899 internal DeviceRequirement[] m_Requirements; 900 internal InputControlList<InputControl> m_Controls; 901 } 902 } 903 904 /// <summary> 905 /// 906 /// </summary> 907 /// <remarks> 908 /// Note that device requirements may require specific controls to be present rather than only requiring 909 /// the presence of a certain type of device. For example, a requirement with a <see cref="controlPath"/> 910 /// of "*/{PrimaryAction}" will be satisfied by any device that has a control marked as <see cref="CommonUsages.PrimaryAction"/>. 911 /// 912 /// Requirements are ordered in a list and can combine with their previous requirement in either <see cref="isAND"> 913 /// AND</see> or in <see cref="isOR">OR</see> fashion. The default is for requirements to combine with AND. 914 /// 915 /// Note that it is not possible to express nested constraints like <c>(a AND b) OR (c AND d)</c>. Also note that 916 /// operator precedence is the opposite of C#, meaning that OR has *higher* precedence than AND. This means 917 /// that <c>a OR b AND c OR d</c> reads as <c>(a OR b) AND (c OR d)</c> (in C# it would read as <c>a OR 918 /// (b AND c) OR d</c>. 919 /// 920 /// More complex expressions can often be expressed differently. For example, <c>(a AND b) OR (c AND d)</c> 921 /// can be expressed as <c>a OR c AND b OR d</c>. 922 /// </remarks> 923 [Serializable] 924 public struct DeviceRequirement : IEquatable<DeviceRequirement> 925 { 926 /// <summary> 927 /// <see cref="InputControlPath">Control path</see> that is matched against a device to determine 928 /// whether it qualifies for the control scheme. 929 /// </summary> 930 /// <remarks> 931 /// </remarks> 932 /// <example> 933 /// <code> 934 /// // A left-hand XR controller. 935 /// "&lt;XRController&gt;{LeftHand}" 936 /// 937 /// // A gamepad. 938 /// "&lt;Gamepad&gt;" 939 /// </code> 940 /// </example> 941 public string controlPath 942 { 943 get => m_ControlPath; 944 set => m_ControlPath = value; 945 } 946 947 /// <summary> 948 /// If true, a device with the given <see cref="controlPath">device path</see> is employed by the 949 /// control scheme if one is available. If none is available, the control scheme is still 950 /// functional. 951 /// </summary> 952 public bool isOptional 953 { 954 get => (m_Flags & Flags.Optional) != 0; 955 set 956 { 957 if (value) 958 m_Flags |= Flags.Optional; 959 else 960 m_Flags &= ~Flags.Optional; 961 } 962 } 963 964 /// <summary> 965 /// Whether the requirement combines with the previous requirement (if any) as a boolean AND. 966 /// </summary> 967 /// <remarks> 968 /// This is the default. For example, to require both a left hand and a right XR controller, 969 /// the first requirement would be for "&lt;XRController&gt;{LeftHand}" and the second 970 /// requirement would be for "&gt;XRController&gt;{RightHand}" and would return true for this 971 /// property. 972 /// </remarks> 973 /// <seealso cref="isOR"/> 974 public bool isAND 975 { 976 get => !isOR; 977 set => isOR = !value; 978 } 979 980 /// <summary> 981 /// Whether the requirement combines with the previous requirement (if any) as a boolean OR. 982 /// </summary> 983 /// <remarks> 984 /// This allows designing control schemes that flexibly work with combinations of devices such that 985 /// if one specific device isn't present, another device can substitute for it. 986 /// 987 /// For example, to design a mouse+keyboard control scheme that can alternatively work with a pen 988 /// instead of a mouse, the first requirement could be for "&lt;Keyboard&gt;", the second one 989 /// could be for "&lt;Mouse&gt;" and the third one could be for "&lt;Pen&gt;" and return true 990 /// for this property. Both the mouse and the pen would be marked as required (i.e. not <see cref="isOptional"/>) 991 /// but the device requirements are satisfied even if either device is present. 992 /// 993 /// Note that if both a pen and a mouse are present at the same time, still only one device is 994 /// picked. In this case, the mouse "wins" as it comes first in the list of requirements. 995 /// </remarks> 996 public bool isOR 997 { 998 get => (m_Flags & Flags.Or) != 0; 999 set 1000 { 1001 if (value) 1002 m_Flags |= Flags.Or; 1003 else 1004 m_Flags &= ~Flags.Or; 1005 } 1006 } 1007 1008 [SerializeField] internal string m_ControlPath; 1009 [SerializeField] internal Flags m_Flags; 1010 1011 [Flags] 1012 internal enum Flags 1013 { 1014 None = 0, 1015 Optional = 1 << 0, 1016 Or = 1 << 1, 1017 } 1018 1019 public override string ToString() 1020 { 1021 if (!string.IsNullOrEmpty(controlPath)) 1022 { 1023 if (isOptional) 1024 return controlPath + " (Optional)"; 1025 return controlPath + " (Required)"; 1026 } 1027 1028 return base.ToString(); 1029 } 1030 1031 public bool Equals(DeviceRequirement other) 1032 { 1033 return string.Equals(m_ControlPath, other.m_ControlPath) && m_Flags == other.m_Flags && 1034 string.Equals(controlPath, other.controlPath) && isOptional == other.isOptional; 1035 } 1036 1037 public override bool Equals(object obj) 1038 { 1039 if (ReferenceEquals(null, obj)) 1040 return false; 1041 1042 return obj is DeviceRequirement && Equals((DeviceRequirement)obj); 1043 } 1044 1045 public override int GetHashCode() 1046 { 1047 unchecked 1048 { 1049 var hashCode = (m_ControlPath != null ? m_ControlPath.GetHashCode() : 0); 1050 hashCode = (hashCode * 397) ^ m_Flags.GetHashCode(); 1051 hashCode = (hashCode * 397) ^ (controlPath != null ? controlPath.GetHashCode() : 0); 1052 hashCode = (hashCode * 397) ^ isOptional.GetHashCode(); 1053 return hashCode; 1054 } 1055 } 1056 1057 public static bool operator==(DeviceRequirement left, DeviceRequirement right) 1058 { 1059 return left.Equals(right); 1060 } 1061 1062 public static bool operator!=(DeviceRequirement left, DeviceRequirement right) 1063 { 1064 return !left.Equals(right); 1065 } 1066 } 1067 1068 /// <summary> 1069 /// JSON-serialized form of a control scheme. 1070 /// </summary> 1071 [Serializable] 1072 internal struct SchemeJson 1073 { 1074 public string name; 1075 public string bindingGroup; 1076 public DeviceJson[] devices; 1077 1078 [Serializable] 1079 public struct DeviceJson 1080 { 1081 public string devicePath; 1082 public bool isOptional; 1083 public bool isOR; 1084 1085 public DeviceRequirement ToDeviceEntry() 1086 { 1087 return new DeviceRequirement 1088 { 1089 controlPath = devicePath, 1090 isOptional = isOptional, 1091 isOR = isOR, 1092 }; 1093 } 1094 1095 public static DeviceJson From(DeviceRequirement requirement) 1096 { 1097 return new DeviceJson 1098 { 1099 devicePath = requirement.controlPath, 1100 isOptional = requirement.isOptional, 1101 isOR = requirement.isOR, 1102 }; 1103 } 1104 } 1105 1106 public InputControlScheme ToScheme() 1107 { 1108 DeviceRequirement[] deviceRequirements = null; 1109 if (devices != null && devices.Length > 0) 1110 { 1111 var count = devices.Length; 1112 deviceRequirements = new DeviceRequirement[count]; 1113 for (var i = 0; i < count; ++i) 1114 deviceRequirements[i] = devices[i].ToDeviceEntry(); 1115 } 1116 1117 return new InputControlScheme 1118 { 1119 m_Name = string.IsNullOrEmpty(name) ? null : name, 1120 m_BindingGroup = string.IsNullOrEmpty(bindingGroup) ? null : bindingGroup, 1121 m_DeviceRequirements = deviceRequirements, 1122 }; 1123 } 1124 1125 public static SchemeJson ToJson(InputControlScheme scheme) 1126 { 1127 DeviceJson[] devices = null; 1128 if (scheme.m_DeviceRequirements != null && scheme.m_DeviceRequirements.Length > 0) 1129 { 1130 var count = scheme.m_DeviceRequirements.Length; 1131 devices = new DeviceJson[count]; 1132 for (var i = 0; i < count; ++i) 1133 devices[i] = DeviceJson.From(scheme.m_DeviceRequirements[i]); 1134 } 1135 1136 return new SchemeJson 1137 { 1138 name = scheme.m_Name, 1139 bindingGroup = scheme.m_BindingGroup, 1140 devices = devices, 1141 }; 1142 } 1143 1144 public static SchemeJson[] ToJson(InputControlScheme[] schemes) 1145 { 1146 if (schemes == null || schemes.Length == 0) 1147 return null; 1148 1149 var count = schemes.Length; 1150 var result = new SchemeJson[count]; 1151 1152 for (var i = 0; i < count; ++i) 1153 result[i] = ToJson(schemes[i]); 1154 1155 return result; 1156 } 1157 1158 public static InputControlScheme[] ToSchemes(SchemeJson[] schemes) 1159 { 1160 if (schemes == null || schemes.Length == 0) 1161 return null; 1162 1163 var count = schemes.Length; 1164 var result = new InputControlScheme[count]; 1165 1166 for (var i = 0; i < count; ++i) 1167 result[i] = schemes[i].ToScheme(); 1168 1169 return result; 1170 } 1171 } 1172 } 1173}