A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections.Generic; 3using System.Linq; 4using NUnit.Framework; 5using UnityEngine.InputSystem.LowLevel; 6using Unity.Collections; 7using Unity.Collections.LowLevel.Unsafe; 8using UnityEngine.Analytics; 9using UnityEngine.InputSystem.Layouts; 10using UnityEngine.InputSystem.Utilities; 11 12#if UNITY_EDITOR 13using UnityEditor; 14#endif 15 16namespace UnityEngine.InputSystem 17{ 18 /// <summary> 19 /// An implementation of <see cref="IInputRuntime"/> for use during tests. 20 /// </summary> 21 /// <remarks> 22 /// This class is only available in the editor and in development players. 23 /// 24 /// The test runtime replaces the services usually supplied by <see cref="UnityEngineInternal.Input.NativeInputSystem"/>. 25 /// </remarks> 26 /// <seealso cref="InputTestFixture.runtime"/> 27 internal class InputTestRuntime : IInputRuntime, IDisposable 28 { 29 public unsafe delegate long DeviceCommandCallback(int deviceId, InputDeviceCommand* command); 30 31 ~InputTestRuntime() 32 { 33 Dispose(); 34 } 35 36 public int AllocateDeviceId() 37 { 38 var result = m_NextDeviceId; 39 ++m_NextDeviceId; 40 return result; 41 } 42 43 public unsafe void Update(InputUpdateType type) 44 { 45 if (!onShouldRunUpdate.Invoke(type)) 46 return; 47 48 lock (m_Lock) 49 { 50 if (type == InputUpdateType.Dynamic && !dontAdvanceUnscaledGameTimeNextDynamicUpdate) 51 { 52 unscaledGameTime += 1 / 30f; 53 dontAdvanceUnscaledGameTimeNextDynamicUpdate = false; 54 } 55 56 if (m_NewDeviceDiscoveries != null && m_NewDeviceDiscoveries.Count > 0) 57 { 58 if (onDeviceDiscovered != null) 59 foreach (var entry in m_NewDeviceDiscoveries) 60 onDeviceDiscovered(entry.Key, entry.Value); 61 m_NewDeviceDiscoveries.Clear(); 62 } 63 64 onBeforeUpdate?.Invoke(type); 65 66 // Advance time *after* onBeforeUpdate so that events generated from onBeforeUpdate 67 // don't get bumped into the following update. 68 if (type == InputUpdateType.Dynamic && !dontAdvanceTimeNextDynamicUpdate) 69 { 70 currentTime += advanceTimeEachDynamicUpdate; 71 dontAdvanceTimeNextDynamicUpdate = false; 72 } 73 74 if (onUpdate != null) 75 { 76 var buffer = new InputEventBuffer( 77 (InputEvent*)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer), 78 m_EventCount, m_EventWritePosition, m_EventBuffer.Length); 79 80 try 81 { 82 onUpdate(type, ref buffer); 83 } 84 catch (Exception e) 85 { 86 // Same order as in NativeInputRuntime 87 Debug.LogException(e); 88 Debug.LogError($"{e.GetType().Name} during event processing of {type} update; resetting event buffer"); 89 90 // Rethrow exception for test runtime to enable us to assert against it in tests. 91 m_EventCount = 0; 92 m_EventWritePosition = 0; 93 throw; 94 } 95 96 m_EventCount = buffer.eventCount; 97 m_EventWritePosition = (int)buffer.sizeInBytes; 98 99 if (NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(buffer.data) != 100 NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_EventBuffer)) 101 m_EventBuffer = buffer.data; 102 } 103 else 104 { 105 m_EventCount = 0; 106 m_EventWritePosition = 0; 107 } 108 } 109 } 110 111 public unsafe void QueueEvent(InputEvent* eventPtr) 112 { 113 var eventSize = eventPtr->sizeInBytes; 114 var alignedEventSize = eventSize.AlignToMultipleOf(4); 115 116 lock (m_Lock) 117 { 118 eventPtr->eventId = m_NextEventId; 119 eventPtr->handled = false; 120 ++m_NextEventId; 121 122 // Enlarge buffer, if we have to. 123 if ((m_EventWritePosition + alignedEventSize) > m_EventBuffer.Length) 124 { 125 var newBufferSize = m_EventBuffer.Length + Mathf.Max((int)alignedEventSize, 1024); 126 var newBuffer = new NativeArray<byte>(newBufferSize, Allocator.Persistent); 127 UnsafeUtility.MemCpy(newBuffer.GetUnsafePtr(), m_EventBuffer.GetUnsafePtr(), m_EventWritePosition); 128 m_EventBuffer.Dispose(); 129 m_EventBuffer = newBuffer; 130 } 131 132 // Copy event. 133 UnsafeUtility.MemCpy((byte*)m_EventBuffer.GetUnsafePtr() + m_EventWritePosition, eventPtr, eventSize); 134 m_EventWritePosition += (int)alignedEventSize; 135 ++m_EventCount; 136 } 137 } 138 139 public unsafe void SetCanRunInBackground(int deviceId) 140 { 141 SetDeviceCommandCallback(deviceId, 142 (id, command) => 143 { 144 if (command->type == QueryCanRunInBackground.Type) 145 { 146 ((QueryCanRunInBackground*)command)->canRunInBackground = true; 147 return InputDeviceCommand.GenericSuccess; 148 } 149 return InputDeviceCommand.GenericFailure; 150 }); 151 } 152 153 public void SetDeviceCommandCallback(InputDevice device, DeviceCommandCallback callback) 154 { 155 SetDeviceCommandCallback(device.deviceId, callback); 156 } 157 158 public void SetDeviceCommandCallback(int deviceId, DeviceCommandCallback callback) 159 { 160 lock (m_Lock) 161 { 162 if (m_DeviceCommandCallbacks == null) 163 m_DeviceCommandCallbacks = new List<KeyValuePair<int, DeviceCommandCallback>>(); 164 else 165 { 166 for (var i = 0; i < m_DeviceCommandCallbacks.Count; ++i) 167 { 168 if (m_DeviceCommandCallbacks[i].Key == deviceId) 169 { 170 m_DeviceCommandCallbacks[i] = new KeyValuePair<int, DeviceCommandCallback>(deviceId, callback); 171 return; 172 } 173 } 174 } 175 m_DeviceCommandCallbacks.Add(new KeyValuePair<int, DeviceCommandCallback>(deviceId, callback)); 176 } 177 } 178 179 public void SetDeviceCommandCallback<TCommand>(int deviceId, TCommand result) 180 where TCommand : struct, IInputDeviceCommandInfo 181 { 182 bool? receivedCommand = null; 183 unsafe 184 { 185 SetDeviceCommandCallback(deviceId, 186 (id, commandPtr) => 187 { 188 if (commandPtr->type == result.typeStatic) 189 { 190 Assert.That(receivedCommand.HasValue, Is.False); 191 receivedCommand = true; 192 UnsafeUtility.MemCpy(commandPtr, UnsafeUtility.AddressOf(ref result), 193 UnsafeUtility.SizeOf<TCommand>()); 194 return InputDeviceCommand.GenericSuccess; 195 } 196 197 return InputDeviceCommand.GenericFailure; 198 }); 199 } 200 } 201 202 public unsafe long DeviceCommand(int deviceId, InputDeviceCommand* commandPtr) 203 { 204 lock (m_Lock) 205 { 206 if (commandPtr->type == QueryPairedUserAccountCommand.Type) 207 { 208 foreach (var pairing in userAccountPairings) 209 { 210 if (pairing.deviceId != deviceId) 211 continue; 212 213 var queryPairedUser = (QueryPairedUserAccountCommand*)commandPtr; 214 queryPairedUser->handle = pairing.userHandle; 215 queryPairedUser->name = pairing.userName; 216 queryPairedUser->id = pairing.userId; 217 return (long)QueryPairedUserAccountCommand.Result.DevicePairedToUserAccount; 218 } 219 } 220 221 var result = InputDeviceCommand.GenericFailure; 222 if (m_DeviceCommandCallbacks != null) 223 foreach (var entry in m_DeviceCommandCallbacks) 224 { 225 if (entry.Key == deviceId) 226 { 227 result = entry.Value(deviceId, commandPtr); 228 if (result >= 0) 229 return result; 230 } 231 } 232 return result; 233 } 234 } 235 236 public void InvokePlayerFocusChanged(bool newFocusState) 237 { 238 m_HasFocus = newFocusState; 239 onPlayerFocusChanged?.Invoke(newFocusState); 240 } 241 242 public void PlayerFocusLost() 243 { 244 InvokePlayerFocusChanged(false); 245 } 246 247 public void PlayerFocusGained() 248 { 249 InvokePlayerFocusChanged(true); 250 } 251 252 public int ReportNewInputDevice(string deviceDescriptor, int deviceId = InputDevice.InvalidDeviceId) 253 { 254 lock (m_Lock) 255 { 256 if (deviceId == InputDevice.InvalidDeviceId) 257 deviceId = AllocateDeviceId(); 258 if (m_NewDeviceDiscoveries == null) 259 m_NewDeviceDiscoveries = new List<KeyValuePair<int, string>>(); 260 m_NewDeviceDiscoveries.Add(new KeyValuePair<int, string>(deviceId, deviceDescriptor)); 261 return deviceId; 262 } 263 } 264 265 public int ReportNewInputDevice(InputDeviceDescription description, int deviceId = InputDevice.InvalidDeviceId, 266 ulong userHandle = 0, string userName = null, string userId = null) 267 { 268 deviceId = ReportNewInputDevice(description.ToJson(), deviceId); 269 270 // If we have user information, automatically set up 271 if (userHandle != 0) 272 AssociateInputDeviceWithUser(deviceId, userHandle, userName, userId); 273 274 return deviceId; 275 } 276 277 public int ReportNewInputDevice<TDevice>(int deviceId = InputDevice.InvalidDeviceId, 278 ulong userHandle = 0, string userName = null, string userId = null) 279 where TDevice : InputDevice 280 { 281 return ReportNewInputDevice( 282 new InputDeviceDescription {deviceClass = typeof(TDevice).Name, interfaceName = "Test"}, deviceId, 283 userHandle, userName, userId); 284 } 285 286 public unsafe void ReportInputDeviceRemoved(int deviceId) 287 { 288 var removeEvent = DeviceRemoveEvent.Create(deviceId); 289 var removeEventPtr = UnsafeUtility.AddressOf(ref removeEvent); 290 QueueEvent((InputEvent*)removeEventPtr); 291 } 292 293 public void ReportInputDeviceRemoved(InputDevice device) 294 { 295 if (device == null) 296 throw new ArgumentNullException(nameof(device)); 297 ReportInputDeviceRemoved(device.deviceId); 298 } 299 300 public void AssociateInputDeviceWithUser(int deviceId, ulong userHandle, string userName = null, string userId = null) 301 { 302 var existingIndex = -1; 303 for (var i = 0; i < userAccountPairings.Count; ++i) 304 if (userAccountPairings[i].deviceId == deviceId) 305 { 306 existingIndex = i; 307 break; 308 } 309 310 if (userHandle == 0) 311 { 312 if (existingIndex != -1) 313 userAccountPairings.RemoveAt(existingIndex); 314 } 315 else if (existingIndex != -1) 316 { 317 userAccountPairings[existingIndex] = 318 new PairedUser 319 { 320 deviceId = deviceId, 321 userHandle = userHandle, 322 userName = userName, 323 userId = userId, 324 }; 325 } 326 else 327 { 328 userAccountPairings.Add( 329 new PairedUser 330 { 331 deviceId = deviceId, 332 userHandle = userHandle, 333 userName = userName, 334 userId = userId, 335 }); 336 } 337 } 338 339 public void AssociateInputDeviceWithUser(InputDevice device, ulong userHandle, string userName = null, string userId = null) 340 { 341 AssociateInputDeviceWithUser(device.deviceId, userHandle, userName, userId); 342 } 343 344 public struct PairedUser 345 { 346 public int deviceId; 347 public ulong userHandle; 348 public string userName; 349 public string userId; 350 } 351 352 public InputUpdateDelegate onUpdate { get; set; } 353 public Action<InputUpdateType> onBeforeUpdate { get; set; } 354 public Func<InputUpdateType, bool> onShouldRunUpdate { get; set; } 355#if UNITY_EDITOR 356 public Action onPlayerLoopInitialization { get; set; } 357#endif 358 public Action<int, string> onDeviceDiscovered { get; set; } 359 public Action onShutdown { get; set; } 360 public Action<bool> onPlayerFocusChanged { get; set; } 361 public bool isPlayerFocused => m_HasFocus; 362 public float pollingFrequency { get; set; } 363 public double currentTime { get; set; } 364 public double currentTimeForFixedUpdate { get; set; } 365 public float unscaledGameTime { get; set; } = 1; 366 public bool dontAdvanceUnscaledGameTimeNextDynamicUpdate { get; set; } 367 368 public double advanceTimeEachDynamicUpdate { get; set; } = 1.0 / 60; 369 370 public bool dontAdvanceTimeNextDynamicUpdate { get; set; } 371 372 public bool runInBackground { get; set; } = false; 373 374 public Vector2 screenSize { get; set; } = new Vector2(1024, 768); 375 public ScreenOrientation screenOrientation { set; get; } = ScreenOrientation.Portrait; 376 public bool normalizeScrollWheelDelta { get; set; } = true; 377 public float scrollWheelDeltaPerTick { get; set; } = 1.0f; 378 379 public List<PairedUser> userAccountPairings 380 { 381 get 382 { 383 if (m_UserPairings == null) 384 m_UserPairings = new List<PairedUser>(); 385 return m_UserPairings; 386 } 387 } 388 389 public void Dispose() 390 { 391 m_EventBuffer.Dispose(); 392 GC.SuppressFinalize(this); 393 } 394 395 public double currentTimeOffsetToRealtimeSinceStartup 396 { 397 get => m_CurrentTimeOffsetToRealtimeSinceStartup; 398 set 399 { 400 m_CurrentTimeOffsetToRealtimeSinceStartup = value; 401 InputRuntime.s_CurrentTimeOffsetToRealtimeSinceStartup = value; 402 } 403 } 404 405 public bool isInBatchMode { get; set; } 406 407 #if UNITY_EDITOR 408 public bool isInPlayMode { get; set; } = true; 409 public bool isPaused { get; set; } 410 public bool isEditorActive { get; set; } = true; 411 public Func<IntPtr, bool> onUnityRemoteMessage 412 { 413 get => m_UnityRemoteMessageHandler; 414 set => m_UnityRemoteMessageHandler = value; 415 } 416 417 public bool? unityRemoteGyroEnabled; 418 public float? unityRemoteGyroUpdateInterval; 419 420 public void SetUnityRemoteGyroEnabled(bool value) 421 { 422 unityRemoteGyroEnabled = value; 423 } 424 425 public void SetUnityRemoteGyroUpdateInterval(float interval) 426 { 427 unityRemoteGyroUpdateInterval = interval; 428 } 429 430 public Action<PlayModeStateChange> onPlayModeChanged { get; set; } 431 public Action onProjectChange { get; set; } 432 #endif 433 434 public int eventCount => m_EventCount; 435 436 internal const int kDefaultEventBufferSize = 1024 * 512; 437 438 private bool m_HasFocus = true; 439 private int m_NextDeviceId = 1; 440 private int m_NextEventId = 1; 441 internal int m_EventCount; 442 private int m_EventWritePosition; 443 private NativeArray<byte> m_EventBuffer = new NativeArray<byte>(kDefaultEventBufferSize, Allocator.Persistent); 444 private List<PairedUser> m_UserPairings; 445 private List<KeyValuePair<int, string>> m_NewDeviceDiscoveries; 446 private List<KeyValuePair<int, DeviceCommandCallback>> m_DeviceCommandCallbacks; 447 private object m_Lock = new object(); 448 private double m_CurrentTimeOffsetToRealtimeSinceStartup; 449 private Func<IntPtr, bool> m_UnityRemoteMessageHandler; 450 451 #if UNITY_ANALYTICS || UNITY_EDITOR 452 453 public Action<string, int, int> onRegisterAnalyticsEvent { get; set; } 454 public Action<string, object> onSendAnalyticsEvent { get; set; } 455 456 public void SendAnalytic(InputAnalytics.IInputAnalytic analytic) 457 { 458 #if UNITY_2023_2_OR_NEWER 459 460 // Mimic editor analytics for Unity 2023.2+ invoking TryGatherData to send 461 var analyticInfoAttribute = analytic.GetType().GetCustomAttributes( 462 typeof(AnalyticInfoAttribute), true).FirstOrDefault() as AnalyticInfoAttribute; 463 var info = analytic.info; 464 #if UNITY_EDITOR 465 // Registration handled by framework 466 #else 467 onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements); // only to avoid writing two tests per Unity version (registration handled by framework) 468 #endif 469 if (analytic.TryGatherData(out var data, out var ex) && data != null && analyticInfoAttribute != null) 470 onSendAnalyticsEvent?.Invoke(analyticInfoAttribute.eventName, data); 471 else if (ex != null) 472 throw ex; // rethrow for visibility in test scope 473 474 #else 475 476 var info = analytic.info; 477 onRegisterAnalyticsEvent?.Invoke(info.Name, info.MaxEventsPerHour, info.MaxNumberOfElements); 478 479 if (analytic.TryGatherData(out var data, out var error)) 480 onSendAnalyticsEvent?.Invoke(info.Name, data); 481 else 482 throw error; // For visibility in tests 483 484 #endif // UNITY_2023_2_OR_NEWER 485 } 486 487 #endif // UNITY_ANALYTICS || UNITY_EDITOR 488 } 489}