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&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 "<Gamepad>" and
76 /// another entry requires a "<Gamepad>", 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 "<Gamepad>/leftStick"
78 /// and another requires "<Gamepad>, 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<InputActionAsset>();
184 ///
185 /// // Add some control schemes to the asset.
186 /// asset.AddControlScheme("KeyboardMouse")
187 /// .WithRequiredDevice<Keyboard>()
188 /// .WithRequiredDevice<Mouse>());
189 /// asset.AddControlScheme("Gamepad")
190 /// .WithRequiredDevice<Gamepad>());
191 /// asset.AddControlScheme("DualGamepad")
192 /// .WithRequiredDevice<Gamepad>())
193 /// .WithOptionalGamepad<Gamepad>());
194 ///
195 /// // Add some devices that we can test with.
196 /// var keyboard = InputSystem.AddDevice<Keyboard>();
197 /// var mouse = InputSystem.AddDevice<Mouse>();
198 /// var gamepad1 = InputSystem.AddDevice<Gamepad>();
199 /// var gamepad2 = InputSystem.AddDevice<Gamepad>();
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>"<Gamepad></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>"<Gamepad>"</c> requirement
680 /// will match a <see cref="DualShock.DualShock4GamepadHID"/> with a <em>lower</em> score than a control
681 /// scheme with a <c>"<DualShockGamepad>"</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 /// "<XRController>{LeftHand}"
936 ///
937 /// // A gamepad.
938 /// "<Gamepad>"
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 "<XRController>{LeftHand}" and the second
970 /// requirement would be for ">XRController>{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 "<Keyboard>", the second one
989 /// could be for "<Mouse>" and the third one could be for "<Pen>" 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}