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}