4 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
8 using System.Reflection;
9 using System.Runtime.Loader;
14 static partial class GameAnalyticsManager
16 public enum ErrorSeverity
26 public enum ProgressionStatus
34 public enum CustomDimensions01
40 public enum CustomDimensions02
55 public enum CustomDimensions03
62 public enum ResourceCurrency
67 public enum ResourceFlowType
74 public enum MoneySource
96 private readonly
static HashSet<string> sentEventIdentifiers =
new HashSet<string>();
98 private class Implementation : IDisposable
100 #region GameAnalytics methods
101 private readonly Action<string, string> initialize;
102 internal void Initialize(
string gameKey,
string secretKey)
103 => initialize(gameKey, secretKey);
105 private readonly Action<string> configureBuild;
106 internal void ConfigureBuild(
string config) => configureBuild(config);
108 private readonly Action<ErrorSeverity, string> addErrorEvent;
109 internal void AddErrorEvent(ErrorSeverity severity,
string message)
110 => addErrorEvent(severity, message);
112 private readonly Action<string, IDictionary<string, object>?> addDesignEvent0;
113 internal void AddDesignEvent(
string message, IDictionary<string, object>? fields =
null)
114 => addDesignEvent0(message, fields);
116 private readonly Action<string, double> addDesignEvent1;
117 internal void AddDesignEvent(
string message,
double value)
118 => addDesignEvent1(message, value);
120 private readonly Action<ProgressionStatus, string> addProgressionEvent01;
121 internal void AddProgressionEvent(ProgressionStatus status,
string progression01)
122 => addProgressionEvent01(status, progression01);
124 private readonly Action<ProgressionStatus, string, double> addProgressionEvent01Score;
125 internal void AddProgressionEvent(ProgressionStatus status,
string progression01,
double score)
126 => addProgressionEvent01Score(status, progression01, score);
128 private readonly Action<ProgressionStatus, string, string> addProgressionEvent02;
129 internal void AddProgressionEvent(ProgressionStatus status,
string progression01,
string progression02)
130 => addProgressionEvent02(status, progression01, progression02);
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);
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);
140 private readonly Action<string> setCustomDimension01;
141 internal void SetCustomDimension01(
string dimension01)
142 => setCustomDimension01(dimension01);
144 private readonly Action<string[]> configureAvailableCustomDimensions01;
145 internal void ConfigureAvailableCustomDimensions01(params CustomDimensions01[] customDimensions)
146 => configureAvailableCustomDimensions01(customDimensions.Select(d => d.ToString()).ToArray());
148 private readonly Action<string> setCustomDimension02;
149 internal void SetCustomDimension02(
string dimension02)
150 => setCustomDimension02(dimension02);
152 private readonly Action<string[]> configureAvailableCustomDimensions02;
153 internal void ConfigureAvailableCustomDimensions02(params CustomDimensions02[] customDimensions)
154 => configureAvailableCustomDimensions02(customDimensions.Select(d => d.ToString()).ToArray());
156 private readonly Action<string[]> configureAvailableResourceCurrencies;
157 internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions)
158 => configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray());
160 private readonly Action<string[]> configureAvailableCustomDimensions03;
161 internal void ConfigureAvailableCustomDimensions03(params CustomDimensions03[] customDimensions)
162 => configureAvailableCustomDimensions03(customDimensions.Select(d => d.ToString()).ToArray());
164 private readonly Action<string> setCustomDimension03;
165 internal void SetCustomDimension03(
string dimension03)
166 => setCustomDimension03(dimension03);
168 private readonly Action<string[]> configureAvailableResourceItemTypes;
169 internal void ConfigureAvailableResourceItemTypes(params
string[] resourceItemTypes)
170 => configureAvailableResourceItemTypes(resourceItemTypes);
172 private readonly Action<bool> setEnabledInfoLog;
173 internal void SetEnabledInfoLog(
bool enabled)
174 => setEnabledInfoLog(enabled);
176 private readonly Action<bool> setEnabledVerboseLog;
177 internal void SetEnabledVerboseLog(
bool enabled)
178 => setEnabledVerboseLog(enabled);
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";
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];
195 private Action Call(MethodInfo methodInfo)
196 => () => methodInfo?.Invoke(
null,
null);
198 private Action<T> Call<T>(MethodInfo methodInfo)
202 methodInfo.Invoke(
null, args1);
205 private Action<T1, T2> Call<T1, T2>(MethodInfo methodInfo)
206 => (T1 arg1, T2 arg2) =>
210 methodInfo.Invoke(
null, args2);
213 private Action<T1, T2, T3> Call<T1, T2, T3>(MethodInfo methodInfo)
214 => (T1 arg1, T2 arg2, T3 arg3) =>
219 methodInfo.Invoke(
null, args3);
222 private Action<T1, T2, T3, T4> Call<T1, T2, T3, T4>(MethodInfo methodInfo)
223 => (T1 arg1, T2 arg2, T3 arg3, T4 arg4) =>
229 methodInfo.Invoke(
null, args4);
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) =>
240 methodInfo.Invoke(
null, args5);
244 private AssemblyLoadContext? loadContext;
245 private Assembly? assembly;
247 private string GetAssemblyPath(
string assemblyName)
249 Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
250 $
"{assemblyName}.dll");
252 private bool resolvingDependency;
253 private Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName)
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;
262 internal Implementation()
264 loadContext =
new AssemblyLoadContext(AssemblyName, isCollectible:
true);
265 loadContext.Resolving += ResolveDependency;
266 assembly = loadContext.LoadFromAssemblyPath(
267 GetAssemblyPath(AssemblyName));
269 Type getType(
string name)
270 => assembly.GetType($
"{Namespace}.{name}")
271 ??
throw new Exception($
"Could not find type\"{Namespace}.{name}\"");
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)}");
278 MethodInfo getMethod(
string name, Type[] types)
280 foreach (var me
in mainClass.GetMethods())
282 var aksjdnakjsdnf = me;
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))}");
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) }));
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) }));
331 onQuit = Call(getMethod(
"OnQuit", Array.Empty<Type>()));
334 private readonly Action? onQuit;
335 private void OnQuit()
339 if (assembly !=
null) { onQuit?.Invoke(); }
343 e = e.GetInnermost();
345 DebugConsole.AddWarning($
"Failed to call GameAnalytics.OnQuit: {e.Message} {e.StackTrace}");
352 public void Dispose()
354 if (loadContext is
null) {
return; }
357 loadContext?.Unload();
362 private static Implementation? loadedImplementation;
364 private static void ValidateEventID(
string eventID)
367 string[] parts = eventID.Split(
':');
368 if (parts.Length > 5)
370 DebugConsole.ThrowError($
"Invalid GameAnalytics event id \"{eventID}\". Only 5 id parts allowed separated by ':'");
372 if (parts.Any(p => p.Length > 32))
374 DebugConsole.ThrowError($
"Invalid GameAnalytics event id \"{eventID}\". Each id part separated by ':' must be 32 characters or less.");
379 public static void AddErrorEvent(ErrorSeverity errorSeverity,
string message)
381 if (!SendUserStatistics) {
return; }
382 loadedImplementation?.AddErrorEvent(errorSeverity, message);
388 public static void AddErrorEventOnce(
string identifier, ErrorSeverity errorSeverity,
string message)
390 if (!SendUserStatistics) {
return; }
391 if (sentEventIdentifiers.Contains(identifier)) {
return; }
393 if (ContentPackageManager.ModsEnabled)
395 message =
"[MODDED] " + message;
398 loadedImplementation?.AddErrorEvent(errorSeverity, message);
399 sentEventIdentifiers.Add(identifier);
402 public static void AddDesignEvent(
string eventID)
404 if (!SendUserStatistics) {
return; }
405 ValidateEventID(eventID);
406 loadedImplementation?.AddDesignEvent(eventID);
409 public static void AddDesignEvent(
string eventID,
double value)
411 if (!SendUserStatistics) {
return; }
412 ValidateEventID(eventID);
413 loadedImplementation?.AddDesignEvent(eventID, value);
416 public static void AddProgressionEvent(ProgressionStatus progressionStatus,
string progression01)
418 if (!SendUserStatistics) {
return; }
419 loadedImplementation?.AddProgressionEvent(progressionStatus, progression01);
422 public static void AddProgressionEvent(ProgressionStatus progressionStatus,
string progression01,
double score)
424 if (!SendUserStatistics) {
return; }
425 loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, score);
428 public static void AddProgressionEvent(ProgressionStatus progressionStatus,
string progression01,
string progression02)
430 if (!SendUserStatistics) {
return; }
431 loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02);
434 public static void AddProgressionEvent(ProgressionStatus progressionStatus,
string progression01,
string progression02,
string progression03)
436 if (!SendUserStatistics) {
return; }
437 loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03);
440 public static void SetCustomDimension01(CustomDimensions01 dimension)
442 if (!SendUserStatistics) {
return; }
443 loadedImplementation?.SetCustomDimension01(dimension.ToString());
446 public static void SetCustomDimension03(CustomDimensions03 dimension)
448 if (!SendUserStatistics) {
return; }
449 loadedImplementation?.SetCustomDimension03(dimension.ToString());
452 public static void SetCurrentLevel(LevelData levelData)
454 if (!SendUserStatistics) {
return; }
456 CustomDimensions02 customDimension = CustomDimensions02.None;
457 if (levelData !=
null)
459 float levelDifficulty = levelData.Difficulty;
460 customDimension = (CustomDimensions02)MathHelper.Clamp((
int)(levelDifficulty / 10) + 1, 0, Enum.GetValues(typeof(CustomDimensions02)).Length - 1);
463 loadedImplementation?.SetCustomDimension02(customDimension.ToString());
466 public static void AddMoneyGainedEvent(
int amount, MoneySource moneySource,
string eventId)
468 AddResourceEvent(ResourceFlowType.Source, ResourceCurrency.Money, amount, moneySource.ToString(), eventId);
471 public static void AddMoneySpentEvent(
int amount, MoneySink moneySink,
string eventId)
473 AddResourceEvent(ResourceFlowType.Sink, ResourceCurrency.Money, amount, moneySink.ToString(), eventId);
476 private static void AddResourceEvent(ResourceFlowType flowType, ResourceCurrency currency,
float amount,
string eventType,
string eventId)
478 if (!SendUserStatistics) {
return; }
479 loadedImplementation?.AddResourceEvent(flowType, currency.ToString(), amount, eventType, eventId);
482 private static void Init()
487 loadedImplementation =
new Implementation();
491 DebugConsole.ThrowError(
"Initializing GameAnalytics failed. Disabling user statistics...", e);
492 SetConsent(Consent.Error);
498 loadedImplementation?.SetEnabledInfoLog(
true);
499 loadedImplementation?.SetEnabledVerboseLog(
true);
503 DebugConsole.ThrowError(
"Initializing GameAnalytics failed. Disabling user statistics...", e);
504 SetConsent(Consent.Error);
509 string exePath = Assembly.GetEntryAssembly()!.Location;
510 string? exeName =
string.Empty;
514 Md5Hash? exeHash =
null;
517 exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect);
521 DebugConsole.ThrowError(
"Error while calculating MD5 hash for the executable \"" + exePath +
"\"", e);
525 string buildConfiguration =
"Release";
527 buildConfiguration =
"Debug";
529 buildConfiguration =
"Unstable";
531 loadedImplementation?.ConfigureBuild(GameMain.Version.ToString()
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());
544 loadedImplementation?.AddDesignEvent(
"Executable:"
545 + GameMain.Version.ToString()
547 + (exeHash?.ShortRepresentation ??
"Unknown") +
":"
548 + AssemblyInfo.GitRevision +
":"
549 + buildConfiguration);
551 SetCustomDimension01(ContentPackageManager.ModsEnabled ? CustomDimensions01.Modded : CustomDimensions01.Vanilla);
553 CustomDimensions03 platform = CustomDimensions03.UnknownPlatform;
554 if (SteamManager.IsInitialized)
556 platform = CustomDimensions03.Steam;
558 else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
560 platform = CustomDimensions03.EGS;
562 SetCustomDimension03(platform);
566 DebugConsole.ThrowError(
"Initializing GameAnalytics failed. Disabling user statistics...", e);
567 SetConsent(Consent.Error);
571 var allPackages = ContentPackageManager.EnabledPackages.All.ToList();
572 if (allPackages?.Count > 0)
574 List<string> packageNames =
new List<string>();
575 foreach (ContentPackage cp
in allPackages)
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);
583 loadedImplementation?.AddDesignEvent(
"AllContentPackages:" +
string.Join(
" ", packageNames));
585 loadedImplementation?.AddDesignEvent(
"Language:" + GameSettings.CurrentConfig.Language);
589 static partial
void InitKeys();
591 public static void ShutDown()
593 loadedImplementation?.Dispose();
594 loadedImplementation =
null;