A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using UnityEditor;
4using UnityEngine.InputSystem.LowLevel;
5using UnityEngine.Serialization;
6
7namespace UnityEngine.InputSystem.Editor
8{
9 /// <summary>
10 /// Analytics record for tracking engagement with Input Action Asset editor(s).
11 /// </summary>
12#if UNITY_2023_2_OR_NEWER
13 [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
14 maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
15#endif // UNITY_2023_2_OR_NEWER
16 internal class InputActionsEditorSessionAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
17 {
18 public const string kEventName = "input_actionasset_editor_closed";
19 public const int kMaxEventsPerHour = 100; // default: 1000
20 public const int kMaxNumberOfElements = 100; // default: 1000
21
22 /// <summary>
23 /// Construct a new <c>InputActionsEditorSession</c> record of the given <para>type</para>.
24 /// </summary>
25 /// <param name="kind">The editor type for which this record is valid.</param>
26 public InputActionsEditorSessionAnalytic(Data.Kind kind)
27 {
28 if (kind == Data.Kind.Invalid)
29 throw new ArgumentException(nameof(kind));
30
31 Initialize(kind);
32 }
33
34 /// <summary>
35 /// Register that an action map edit has occurred.
36 /// </summary>
37 public void RegisterActionMapEdit()
38 {
39 if (ImplicitFocus())
40 ++m_Data.action_map_modification_count;
41 }
42
43 /// <summary>
44 /// Register that an action edit has occurred.
45 /// </summary>
46 public void RegisterActionEdit()
47 {
48 if (ImplicitFocus() && ComputeDuration() > 0.5) // Avoid logging actions triggered via UI initialization
49 ++m_Data.action_modification_count;
50 }
51
52 /// <summary>
53 /// Register than a binding edit has occurred.
54 /// </summary>
55 public void RegisterBindingEdit()
56 {
57 if (ImplicitFocus())
58 ++m_Data.binding_modification_count;
59 }
60
61 /// <summary>
62 /// Register that a control scheme edit has occurred.
63 /// </summary>
64 public void RegisterControlSchemeEdit()
65 {
66 if (ImplicitFocus())
67 ++m_Data.control_scheme_modification_count;
68 }
69
70 /// <summary>
71 /// Register that the editor has received focus which is expected to reflect that the user
72 /// is currently exploring or editing it.
73 /// </summary>
74 public void RegisterEditorFocusIn()
75 {
76 if (!hasSession || hasFocus)
77 return;
78
79 m_FocusStart = currentTime;
80 }
81
82 /// <summary>
83 /// Register that the editor has lost focus which is expected to reflect that the user currently
84 /// has the attention elsewhere.
85 /// </summary>
86 /// <remarks>
87 /// Calling this method without having an ongoing session and having focus will not have any effect.
88 /// </remarks>
89 public void RegisterEditorFocusOut()
90 {
91 if (!hasSession || !hasFocus)
92 return;
93
94 var duration = currentTime - m_FocusStart;
95 m_FocusStart = float.NaN;
96 m_Data.session_focus_duration_seconds += (float)duration;
97 ++m_Data.session_focus_switch_count;
98 }
99
100 /// <summary>
101 /// Register a user-event related to explicitly saving in the editor, e.g.
102 /// using a button, menu or short-cut to trigger the save command.
103 /// </summary>
104 public void RegisterExplicitSave()
105 {
106 if (!hasSession)
107 return; // No pending session
108
109 ++m_Data.explicit_save_count;
110 }
111
112 /// <summary>
113 /// Register a user-event related to implicitly saving in the editor, e.g.
114 /// by having auto-save enabled and indirectly saving the associated asset.
115 /// </summary>
116 public void RegisterAutoSave()
117 {
118 if (!hasSession)
119 return; // No pending session
120
121 ++m_Data.auto_save_count;
122 }
123
124 /// <summary>
125 /// Register a user-event related to resetting the editor action configuration to defaults.
126 /// </summary>
127 public void RegisterReset()
128 {
129 if (!hasSession)
130 return; // No pending session
131
132 ++m_Data.reset_count;
133 }
134
135 /// <summary>
136 /// Begins a new session if the session has not already been started.
137 /// </summary>
138 /// <remarks>
139 /// If the session has already been started due to a previous call to <see cref="Begin()"/> without
140 /// a call to <see cref="End()"/> this method has no effect.
141 /// </remarks>
142 public void Begin()
143 {
144 if (hasSession)
145 return; // Session already started.
146
147 m_SessionStart = currentTime;
148 }
149
150 /// <summary>
151 /// Ends the current session.
152 /// </summary>
153 /// <remarks>
154 /// If the session has not previously been started via a call to <see cref="Begin()"/> calling this
155 /// method has no effect.
156 /// </remarks>
157 public void End()
158 {
159 if (!hasSession)
160 return; // No pending session
161
162 // Make sure we register focus out if failed to capture or not invoked
163 if (hasFocus)
164 RegisterEditorFocusOut();
165
166 // Compute and record total session duration
167 var duration = ComputeDuration();
168 m_Data.session_duration_seconds += duration;
169
170 // Sanity check data, if less than a second its likely a glitch so avoid sending incorrect data
171 // Send analytics event
172 if (duration >= 1.0)
173 runtime.SendAnalytic(this);
174
175 // Reset to allow instance to be reused
176 Initialize(m_Data.kind);
177 }
178
179 #region IInputAnalytic Interface
180
181#if UNITY_2023_2_OR_NEWER
182 public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
183#else
184 public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
185#endif
186 {
187 if (!isValid)
188 {
189 data = null;
190 error = new Exception("Unable to gather data without a valid session");
191 return false;
192 }
193
194 data = this.m_Data;
195 error = null;
196 return true;
197 }
198
199 public InputAnalytics.InputAnalyticInfo info => new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
200
201 #endregion
202
203 private double ComputeDuration() => hasSession ? currentTime - m_SessionStart : 0.0;
204
205 private void Initialize(Data.Kind kind)
206 {
207 m_FocusStart = float.NaN;
208 m_SessionStart = float.NaN;
209
210 m_Data = new Data(kind);
211 }
212
213 private bool ImplicitFocus()
214 {
215 if (!hasSession)
216 return false;
217 if (!hasFocus)
218 RegisterEditorFocusIn();
219 return true;
220 }
221
222 private Data m_Data;
223 private double m_FocusStart;
224 private double m_SessionStart;
225
226 private static IInputRuntime runtime => InputSystem.s_Manager.m_Runtime;
227 private bool hasFocus => !double.IsNaN(m_FocusStart);
228 private bool hasSession => !double.IsNaN(m_SessionStart);
229 // Returns current time since startup. Note that IInputRuntime explicitly defines in interface that
230 // IInputRuntime.currentTime corresponds to EditorApplication.timeSinceStartup in editor.
231 private double currentTime => runtime.currentTime;
232 private bool isValid => m_Data.session_duration_seconds >= 0;
233
234 [Serializable]
235 public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
236 {
237 /// <summary>
238 /// Represents an editor type.
239 /// </summary>
240 /// <remarks>
241 /// This may be added to in the future but items may never be removed.
242 /// </remarks>
243 [Serializable]
244 public enum Kind
245 {
246 Invalid = 0,
247 EditorWindow = 1,
248 EmbeddedInProjectSettings = 2
249 }
250
251 /// <summary>
252 /// Constructs a <c>InputActionsEditorSessionData</c>.
253 /// </summary>
254 /// <param name="kind">Specifies the kind of editor metrics is being collected for.</param>
255 public Data(Kind kind)
256 {
257 this.kind = kind;
258 session_duration_seconds = 0;
259 session_focus_duration_seconds = 0;
260 session_focus_switch_count = 0;
261 action_map_modification_count = 0;
262 action_modification_count = 0;
263 binding_modification_count = 0;
264 explicit_save_count = 0;
265 auto_save_count = 0;
266 reset_count = 0;
267 control_scheme_modification_count = 0;
268 }
269
270 /// <summary>
271 /// Specifies what kind of Input Actions editor this event represents.
272 /// </summary>
273 public Kind kind;
274
275 /// <summary>
276 /// The total duration for the session, i.e. the duration during which the editor window was open.
277 /// </summary>
278 public double session_duration_seconds;
279
280 /// <summary>
281 /// The total duration for which the editor window was open and had focus.
282 /// </summary>
283 public double session_focus_duration_seconds;
284
285 /// <summary>
286 /// Specifies the number of times the window has transitioned from not having focus to having focus in a single session.
287 /// </summary>
288 public int session_focus_switch_count;
289
290 /// <summary>
291 /// The total number of action map modifications during the session.
292 /// </summary>
293 public int action_map_modification_count;
294
295 /// <summary>
296 /// The total number of action modifications during the session.
297 /// </summary>
298 public int action_modification_count;
299
300 /// The total number of binding modifications during the session.
301 /// </summary>
302 public int binding_modification_count;
303
304 /// <summary>
305 /// The total number of controls scheme modifications during the session.
306 /// </summary>
307 public int control_scheme_modification_count;
308
309 /// <summary>
310 /// The total number of explicit saves during the session, i.e. as in user-initiated save.
311 /// </summary>
312 public int explicit_save_count;
313
314 /// <summary>
315 /// The total number of automatic saves during the session, i.e. as in auto-save on close or focus-lost.
316 /// </summary>
317 public int auto_save_count;
318
319 /// <summary>
320 /// The total number of user-initiated resets during the session, i.e. as in using Reset option in menu.
321 /// </summary>
322 public int reset_count;
323
324 public bool isValid => kind != Kind.Invalid && session_duration_seconds >= 0;
325 }
326 }
327}
328#endif