Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs
1 #nullable enable
2 using Barotrauma.IO;
3 using Barotrauma.Steam;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.Loader;
10 using System.Text;
11 
12 namespace Barotrauma
13 {
14  static partial class GameAnalyticsManager
15  {
16  public enum ErrorSeverity
17  {
18  Undefined = 0,
19  Debug = 1,
20  Info = 2,
21  Warning = 3,
22  Error = 4,
23  Critical = 5
24  }
25 
26  public enum ProgressionStatus
27  {
28  Undefined = 0,
29  Start = 1,
30  Complete = 2,
31  Fail = 3
32  }
33 
34  public enum CustomDimensions01
35  {
36  Vanilla,
37  Modded
38  }
39 
40  public enum CustomDimensions02
41  {
42  None,
43  Difficulty0to10,
44  Difficulty10to20,
45  Difficulty20to30,
46  Difficulty30to40,
47  Difficulty40to50,
48  Difficulty50to60,
49  Difficulty60to70,
50  Difficulty70to80,
51  Difficulty80to90,
52  Difficulty90to100,
53  }
54 
55  public enum CustomDimensions03
56  {
57  UnknownPlatform,
58  Steam,
59  EGS
60  }
61 
62  public enum ResourceCurrency
63  {
64  Money
65  }
66 
67  public enum ResourceFlowType
68  {
69  Undefined = 0,
70  Source = 1,
71  Sink = 2
72  }
73 
74  public enum MoneySource
75  {
76  Unknown,
77  MissionReward,
78  Store,
79  Event,
80  Ability,
81  Cheat
82  }
83 
84  public enum MoneySink
85  {
86  Unknown,
87  Store,
88  Service,
89  Crew,
90  SubmarineUpgrade,
91  SubmarineWeapon,
92  SubmarinePurchase,
93  SubmarineSwitch
94  }
95 
96  private readonly static HashSet<string> sentEventIdentifiers = new HashSet<string>();
97 
98  private class Implementation : IDisposable
99  {
100  #region GameAnalytics methods
101  private readonly Action<string, string> initialize;
102  internal void Initialize(string gameKey, string secretKey)
103  => initialize(gameKey, secretKey);
104 
105  private readonly Action<string> configureBuild;
106  internal void ConfigureBuild(string config) => configureBuild(config);
107 
108  private readonly Action<ErrorSeverity, string> addErrorEvent;
109  internal void AddErrorEvent(ErrorSeverity severity, string message)
110  => addErrorEvent(severity, message);
111 
112  private readonly Action<string, IDictionary<string, object>?> addDesignEvent0;
113  internal void AddDesignEvent(string message, IDictionary<string, object>? fields = null)
114  => addDesignEvent0(message, fields);
115 
116  private readonly Action<string, double> addDesignEvent1;
117  internal void AddDesignEvent(string message, double value)
118  => addDesignEvent1(message, value);
119 
120  private readonly Action<ProgressionStatus, string> addProgressionEvent01;
121  internal void AddProgressionEvent(ProgressionStatus status, string progression01)
122  => addProgressionEvent01(status, progression01);
123 
124  private readonly Action<ProgressionStatus, string, double> addProgressionEvent01Score;
125  internal void AddProgressionEvent(ProgressionStatus status, string progression01, double score)
126  => addProgressionEvent01Score(status, progression01, score);
127 
128  private readonly Action<ProgressionStatus, string, string> addProgressionEvent02;
129  internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02)
130  => addProgressionEvent02(status, progression01, progression02);
131 
132  private readonly Action<ProgressionStatus, string, string, string> addProgressionEvent03;
133  internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03)
134  => addProgressionEvent03(status, progression01, progression02, progression03);
135 
136  private readonly Action<ResourceFlowType, string, float, string, string> addResourceEvent;
137  internal void AddResourceEvent(ResourceFlowType flowType, string currency, float amount, string itemType, string itemId)
138  => addResourceEvent(flowType, currency, amount, itemType, itemId);
139 
140  private readonly Action<string> setCustomDimension01;
141  internal void SetCustomDimension01(string dimension01)
142  => setCustomDimension01(dimension01);
143 
144  private readonly Action<string[]> configureAvailableCustomDimensions01;
145  internal void ConfigureAvailableCustomDimensions01(params CustomDimensions01[] customDimensions)
146  => configureAvailableCustomDimensions01(customDimensions.Select(d => d.ToString()).ToArray());
147 
148  private readonly Action<string> setCustomDimension02;
149  internal void SetCustomDimension02(string dimension02)
150  => setCustomDimension02(dimension02);
151 
152  private readonly Action<string[]> configureAvailableCustomDimensions02;
153  internal void ConfigureAvailableCustomDimensions02(params CustomDimensions02[] customDimensions)
154  => configureAvailableCustomDimensions02(customDimensions.Select(d => d.ToString()).ToArray());
155 
156  private readonly Action<string[]> configureAvailableResourceCurrencies;
157  internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions)
158  => configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray());
159 
160  private readonly Action<string[]> configureAvailableCustomDimensions03;
161  internal void ConfigureAvailableCustomDimensions03(params CustomDimensions03[] customDimensions)
162  => configureAvailableCustomDimensions03(customDimensions.Select(d => d.ToString()).ToArray());
163 
164  private readonly Action<string> setCustomDimension03;
165  internal void SetCustomDimension03(string dimension03)
166  => setCustomDimension03(dimension03);
167 
168  private readonly Action<string[]> configureAvailableResourceItemTypes;
169  internal void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes)
170  => configureAvailableResourceItemTypes(resourceItemTypes);
171 
172  private readonly Action<bool> setEnabledInfoLog;
173  internal void SetEnabledInfoLog(bool enabled)
174  => setEnabledInfoLog(enabled);
175 
176  private readonly Action<bool> setEnabledVerboseLog;
177  internal void SetEnabledVerboseLog(bool enabled)
178  => setEnabledVerboseLog(enabled);
179  #endregion
180 
181  #region Data required to fetch methods via reflection
182  private const string AssemblyName = "GameAnalytics.NetStandard";
183  private const string Namespace = "GameAnalyticsSDK.Net";
184  private const string MainClass = "GameAnalytics";
185  private const string EnumPrefix = "EGA";
186  #endregion
187 
188  #region Call implementations
189  private readonly object?[] args1 = new object?[1];
190  private readonly object?[] args2 = new object?[2];
191  private readonly object?[] args3 = new object?[3];
192  private readonly object?[] args4 = new object?[4];
193  private readonly object?[] args5 = new object?[5];
194 
195  private Action Call(MethodInfo methodInfo)
196  => () => methodInfo?.Invoke(null, null);
197 
198  private Action<T> Call<T>(MethodInfo methodInfo)
199  => (T arg1) =>
200  {
201  args1[0] = arg1;
202  methodInfo.Invoke(null, args1);
203  };
204 
205  private Action<T1, T2> Call<T1, T2>(MethodInfo methodInfo)
206  => (T1 arg1, T2 arg2) =>
207  {
208  args2[0] = arg1;
209  args2[1] = arg2;
210  methodInfo.Invoke(null, args2);
211  };
212 
213  private Action<T1, T2, T3> Call<T1, T2, T3>(MethodInfo methodInfo)
214  => (T1 arg1, T2 arg2, T3 arg3) =>
215  {
216  args3[0] = arg1;
217  args3[1] = arg2;
218  args3[2] = arg3;
219  methodInfo.Invoke(null, args3);
220  };
221 
222  private Action<T1, T2, T3, T4> Call<T1, T2, T3, T4>(MethodInfo methodInfo)
223  => (T1 arg1, T2 arg2, T3 arg3, T4 arg4) =>
224  {
225  args4[0] = arg1;
226  args4[1] = arg2;
227  args4[2] = arg3;
228  args4[3] = arg4;
229  methodInfo.Invoke(null, args4);
230  };
231 
232  private Action<T1, T2, T3, T4, T5> Call<T1, T2, T3, T4, T5>(MethodInfo methodInfo)
233  => (T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) =>
234  {
235  args5[0] = arg1;
236  args5[1] = arg2;
237  args5[2] = arg3;
238  args5[3] = arg4;
239  args5[4] = arg5;
240  methodInfo.Invoke(null, args5);
241  };
242  #endregion
243 
244  private AssemblyLoadContext? loadContext;
245  private Assembly? assembly;
246 
247  private string GetAssemblyPath(string assemblyName)
248  => Path.Combine(
249  Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
250  $"{assemblyName}.dll");
251 
252  private bool resolvingDependency;
253  private Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName)
254  {
255  if (resolvingDependency) { return null; }
256  resolvingDependency = true;
257  Assembly dependency = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null")));
258  resolvingDependency = false;
259  return dependency;
260  }
261 
262  internal Implementation()
263  {
264  loadContext = new AssemblyLoadContext(AssemblyName, isCollectible: true);
265  loadContext.Resolving += ResolveDependency;
266  assembly = loadContext.LoadFromAssemblyPath(
267  GetAssemblyPath(AssemblyName));
268 
269  Type getType(string name)
270  => assembly.GetType($"{Namespace}.{name}")
271  ?? throw new Exception($"Could not find type\"{Namespace}.{name}\"");
272 
273  var mainClass = getType(MainClass);
274  var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}");
275  var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}");
276  var resourceFlowTypeEnumType = getType($"{EnumPrefix}{nameof(ResourceFlowType)}");
277 
278  MethodInfo getMethod(string name, Type[] types)
279  {
280  foreach (var me in mainClass.GetMethods())
281  {
282  var aksjdnakjsdnf = me;
283  }
284 
285  return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null)
286  ?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}");
287  }
288 
289  initialize = Call<string, string>(getMethod(nameof(Initialize),
290  new Type[] { typeof(string), typeof(string) }));
291  configureBuild = Call<string>(getMethod(nameof(ConfigureBuild),
292  new Type[] { typeof(string) }));
293  addErrorEvent = Call<ErrorSeverity, string>(getMethod(nameof(AddErrorEvent),
294  new Type[] { errorSeverityEnumType, typeof(string) }));
295  addDesignEvent0 = Call<string, IDictionary<string, object>?>(getMethod(nameof(AddDesignEvent),
296  new Type[] { typeof(string), typeof(IDictionary<string, object>) }));
297  addDesignEvent1 = Call<string, double>(getMethod(nameof(AddDesignEvent),
298  new Type[] { typeof(string), typeof(double) }));
299  addProgressionEvent01 = Call<ProgressionStatus, string>(getMethod(nameof(AddProgressionEvent),
300  new Type[] { progressionStatusEnumType, typeof(string) }));
301  addProgressionEvent01Score = Call<ProgressionStatus, string, double>(getMethod(nameof(AddProgressionEvent),
302  new Type[] { progressionStatusEnumType, typeof(string), typeof(double) }));
303  addProgressionEvent02 = Call<ProgressionStatus, string, string>(getMethod(nameof(AddProgressionEvent),
304  new Type[] { progressionStatusEnumType, typeof(string), typeof(string) }));
305  addProgressionEvent03 = Call<ProgressionStatus, string, string, string>(getMethod(nameof(AddProgressionEvent),
306  new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) }));
307 
308  setCustomDimension01 = Call<string>(getMethod(nameof(SetCustomDimension01),
309  new Type[] { typeof(string) }));
310  configureAvailableCustomDimensions01 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions01),
311  new Type[] { typeof(string[]) }));
312  setCustomDimension02 = Call<string>(getMethod(nameof(SetCustomDimension02),
313  new Type[] { typeof(string) }));
314  configureAvailableCustomDimensions02 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions02),
315  new Type[] { typeof(string[]) }));
316  configureAvailableCustomDimensions03 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions03),
317  new Type[] { typeof(string[]) }));
318  setCustomDimension03 = Call<string>(getMethod(nameof(SetCustomDimension03),
319  new Type[] { typeof(string) }));
320  configureAvailableResourceCurrencies = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceCurrencies),
321  new Type[] { typeof(string[]) }));
322  configureAvailableResourceItemTypes = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceItemTypes),
323  new Type[] { typeof(string[]) }));
324  addResourceEvent = Call<ResourceFlowType, string, float, string, string>(getMethod(nameof(AddResourceEvent),
325  new Type[] { resourceFlowTypeEnumType, typeof(string), typeof(float), typeof(string), typeof(string) }));
326  setEnabledInfoLog = Call<bool>(getMethod(nameof(SetEnabledInfoLog),
327  new Type[] { typeof(bool) }));
328  setEnabledVerboseLog = Call<bool>(getMethod(nameof(SetEnabledVerboseLog),
329  new Type[] { typeof(bool) }));
330 
331  onQuit = Call(getMethod("OnQuit", Array.Empty<Type>()));
332  }
333 
334  private readonly Action? onQuit;
335  private void OnQuit()
336  {
337  try
338  {
339  if (assembly != null) { onQuit?.Invoke(); }
340  }
341  catch (Exception e)
342  {
343  e = e.GetInnermost();
344 
345  DebugConsole.AddWarning($"Failed to call GameAnalytics.OnQuit: {e.Message} {e.StackTrace}");
346  //If this happens then GameAnalytics is just broken,
347  //let's just hope that it uninitialized correctly and
348  //allow the game to keep running
349  }
350  }
351 
352  public void Dispose()
353  {
354  if (loadContext is null) { return; }
355 
356  OnQuit();
357  loadContext?.Unload();
358  loadContext = null;
359  assembly = null;
360  }
361  }
362  private static Implementation? loadedImplementation;
363 
364  private static void ValidateEventID(string eventID)
365  {
366 #if DEBUG
367  string[] parts = eventID.Split(':');
368  if (parts.Length > 5)
369  {
370  DebugConsole.ThrowError($"Invalid GameAnalytics event id \"{eventID}\". Only 5 id parts allowed separated by ':'");
371  }
372  if (parts.Any(p => p.Length > 32))
373  {
374  DebugConsole.ThrowError($"Invalid GameAnalytics event id \"{eventID}\". Each id part separated by ':' must be 32 characters or less.");
375  }
376 #endif
377  }
378 
379  public static void AddErrorEvent(ErrorSeverity errorSeverity, string message)
380  {
381  if (!SendUserStatistics) { return; }
382  loadedImplementation?.AddErrorEvent(errorSeverity, message);
383  }
384 
388  public static void AddErrorEventOnce(string identifier, ErrorSeverity errorSeverity, string message)
389  {
390  if (!SendUserStatistics) { return; }
391  if (sentEventIdentifiers.Contains(identifier)) { return; }
392 
393  if (ContentPackageManager.ModsEnabled)
394  {
395  message = "[MODDED] " + message;
396  }
397 
398  loadedImplementation?.AddErrorEvent(errorSeverity, message);
399  sentEventIdentifiers.Add(identifier);
400  }
401 
402  public static void AddDesignEvent(string eventID)
403  {
404  if (!SendUserStatistics) { return; }
405  ValidateEventID(eventID);
406  loadedImplementation?.AddDesignEvent(eventID);
407  }
408 
409  public static void AddDesignEvent(string eventID, double value)
410  {
411  if (!SendUserStatistics) { return; }
412  ValidateEventID(eventID);
413  loadedImplementation?.AddDesignEvent(eventID, value);
414  }
415 
416  public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01)
417  {
418  if (!SendUserStatistics) { return; }
419  loadedImplementation?.AddProgressionEvent(progressionStatus, progression01);
420  }
421 
422  public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, double score)
423  {
424  if (!SendUserStatistics) { return; }
425  loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, score);
426  }
427 
428  public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02)
429  {
430  if (!SendUserStatistics) { return; }
431  loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02);
432  }
433 
434  public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02, string progression03)
435  {
436  if (!SendUserStatistics) { return; }
437  loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03);
438  }
439 
440  public static void SetCustomDimension01(CustomDimensions01 dimension)
441  {
442  if (!SendUserStatistics) { return; }
443  loadedImplementation?.SetCustomDimension01(dimension.ToString());
444  }
445 
446  public static void SetCustomDimension03(CustomDimensions03 dimension)
447  {
448  if (!SendUserStatistics) { return; }
449  loadedImplementation?.SetCustomDimension03(dimension.ToString());
450  }
451 
452  public static void SetCurrentLevel(LevelData levelData)
453  {
454  if (!SendUserStatistics) { return; }
455 
456  CustomDimensions02 customDimension = CustomDimensions02.None;
457  if (levelData != null)
458  {
459  float levelDifficulty = levelData.Difficulty;
460  customDimension = (CustomDimensions02)MathHelper.Clamp((int)(levelDifficulty / 10) + 1, 0, Enum.GetValues(typeof(CustomDimensions02)).Length - 1);
461  }
462 
463  loadedImplementation?.SetCustomDimension02(customDimension.ToString());
464  }
465 
466  public static void AddMoneyGainedEvent(int amount, MoneySource moneySource, string eventId)
467  {
468  AddResourceEvent(ResourceFlowType.Source, ResourceCurrency.Money, amount, moneySource.ToString(), eventId);
469  }
470 
471  public static void AddMoneySpentEvent(int amount, MoneySink moneySink, string eventId)
472  {
473  AddResourceEvent(ResourceFlowType.Sink, ResourceCurrency.Money, amount, moneySink.ToString(), eventId);
474  }
475 
476  private static void AddResourceEvent(ResourceFlowType flowType, ResourceCurrency currency, float amount, string eventType, string eventId)
477  {
478  if (!SendUserStatistics) { return; }
479  loadedImplementation?.AddResourceEvent(flowType, currency.ToString(), amount, eventType, eventId);
480  }
481 
482  private static void Init()
483  {
484  ShutDown();
485  try
486  {
487  loadedImplementation = new Implementation();
488  }
489  catch (Exception e)
490  {
491  DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
492  SetConsent(Consent.Error);
493  return;
494  }
495 #if DEBUG
496  try
497  {
498  loadedImplementation?.SetEnabledInfoLog(true);
499  loadedImplementation?.SetEnabledVerboseLog(true);
500  }
501  catch (Exception e)
502  {
503  DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
504  SetConsent(Consent.Error);
505  return;
506  }
507 #endif
508 
509  string exePath = Assembly.GetEntryAssembly()!.Location;
510  string? exeName = string.Empty;
511 #if SERVER
512  exeName = "s";
513 #endif
514  Md5Hash? exeHash = null;
515  try
516  {
517  exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect);
518  }
519  catch (Exception e)
520  {
521  DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e);
522  }
523  try
524  {
525  string buildConfiguration = "Release";
526 #if DEBUG
527  buildConfiguration = "Debug";
528 #elif UNSTABLE
529  buildConfiguration = "Unstable";
530 #endif
531  loadedImplementation?.ConfigureBuild(GameMain.Version.ToString()
532  + exeName + ":"
533  + AssemblyInfo.GitRevision + ":"
534  + buildConfiguration);
535  loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast<CustomDimensions01>().ToArray());
536  loadedImplementation?.ConfigureAvailableCustomDimensions02(Enum.GetValues(typeof(CustomDimensions02)).Cast<CustomDimensions02>().ToArray());
537  loadedImplementation?.ConfigureAvailableCustomDimensions03(Enum.GetValues(typeof(CustomDimensions03)).Cast<CustomDimensions03>().ToArray());
538  loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast<ResourceCurrency>().ToArray());
539  loadedImplementation?.ConfigureAvailableResourceItemTypes(
540  Enum.GetValues(typeof(MoneySink)).Cast<MoneySink>().Select(s => s.ToString()).Union(Enum.GetValues(typeof(MoneySource)).Cast<MoneySource>().Select(s => s.ToString())).ToArray());
541 
542  InitKeys();
543 
544  loadedImplementation?.AddDesignEvent("Executable:"
545  + GameMain.Version.ToString()
546  + exeName + ":"
547  + (exeHash?.ShortRepresentation ?? "Unknown") + ":"
548  + AssemblyInfo.GitRevision + ":"
549  + buildConfiguration);
550 
551  SetCustomDimension01(ContentPackageManager.ModsEnabled ? CustomDimensions01.Modded : CustomDimensions01.Vanilla);
552 
553  CustomDimensions03 platform = CustomDimensions03.UnknownPlatform;
554  if (SteamManager.IsInitialized)
555  {
556  platform = CustomDimensions03.Steam;
557  }
558  else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
559  {
560  platform = CustomDimensions03.EGS;
561  }
562  SetCustomDimension03(platform);
563  }
564  catch (Exception e)
565  {
566  DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
567  SetConsent(Consent.Error);
568  return;
569  }
570 
571  var allPackages = ContentPackageManager.EnabledPackages.All.ToList();
572  if (allPackages?.Count > 0)
573  {
574  List<string> packageNames = new List<string>();
575  foreach (ContentPackage cp in allPackages)
576  {
577  string sanitizedName = cp.Name.Replace(":", "").Replace(" ", "");
578  sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length));
579  packageNames.Add(sanitizedName);
580  loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName);
581  }
582  packageNames.Sort();
583  loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames));
584  }
585  loadedImplementation?.AddDesignEvent("Language:" + GameSettings.CurrentConfig.Language);
586 
587  }
588 
589  static partial void InitKeys();
590 
591  public static void ShutDown()
592  {
593  loadedImplementation?.Dispose();
594  loadedImplementation = null;
595  }
596  }
597 }