Files
2025-03-12 12:56:27 +00:00

625 lines
27 KiB
C#

#nullable enable
using Barotrauma.IO;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
namespace Barotrauma
{
static partial class GameAnalyticsManager
{
public enum ErrorSeverity
{
Undefined = 0,
Debug = 1,
Info = 2,
Warning = 3,
Error = 4,
Critical = 5
}
public enum ProgressionStatus
{
Undefined = 0,
Start = 1,
Complete = 2,
Fail = 3
}
public enum CustomDimensions01
{
Vanilla,
Modded
}
public enum CustomDimensions02
{
None,
Difficulty0to10,
Difficulty10to20,
Difficulty20to30,
Difficulty30to40,
Difficulty40to50,
Difficulty50to60,
Difficulty60to70,
Difficulty70to80,
Difficulty80to90,
Difficulty90to100,
}
public enum CustomDimensions03
{
UnknownPlatform,
Steam,
EGS
}
public enum ResourceCurrency
{
Money
}
public enum ResourceFlowType
{
Undefined = 0,
Source = 1,
Sink = 2
}
public enum MoneySource
{
Unknown,
MissionReward,
Store,
Event,
Ability,
Cheat
}
public enum MoneySink
{
Unknown,
Store,
Service,
Crew,
SubmarineUpgrade,
SubmarineWeapon,
SubmarinePurchase,
SubmarineSwitch
}
private readonly static HashSet<string> sentEventIdentifiers = new HashSet<string>();
private class Implementation : IDisposable
{
#region GameAnalytics methods
private readonly Action<string, string> initialize;
internal void Initialize(string gameKey, string secretKey)
=> initialize(gameKey, secretKey);
private readonly Action<string> configureBuild;
internal void ConfigureBuild(string config) => configureBuild(config);
private readonly Action<ErrorSeverity, string> addErrorEvent;
internal void AddErrorEvent(ErrorSeverity severity, string message)
=> addErrorEvent(severity, message);
private readonly Action<string, IDictionary<string, object>?> addDesignEvent0;
internal void AddDesignEvent(string message, IDictionary<string, object>? fields = null)
=> addDesignEvent0(message, fields);
private readonly Action<string, double> addDesignEvent1;
internal void AddDesignEvent(string message, double value)
=> addDesignEvent1(message, value);
private readonly Action<ProgressionStatus, string> addProgressionEvent01;
internal void AddProgressionEvent(ProgressionStatus status, string progression01)
=> addProgressionEvent01(status, progression01);
private readonly Action<ProgressionStatus, string, double> addProgressionEvent01Score;
internal void AddProgressionEvent(ProgressionStatus status, string progression01, double score)
=> addProgressionEvent01Score(status, progression01, score);
private readonly Action<ProgressionStatus, string, string> addProgressionEvent02;
internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02)
=> addProgressionEvent02(status, progression01, progression02);
private readonly Action<ProgressionStatus, string, string, string> addProgressionEvent03;
internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03)
=> addProgressionEvent03(status, progression01, progression02, progression03);
private readonly Action<ResourceFlowType, string, float, string, string> addResourceEvent;
internal void AddResourceEvent(ResourceFlowType flowType, string currency, float amount, string itemType, string itemId)
=> addResourceEvent(flowType, currency, amount, itemType, itemId);
private readonly Action<string> setCustomDimension01;
internal void SetCustomDimension01(string dimension01)
=> setCustomDimension01(dimension01);
private readonly Action<string[]> configureAvailableCustomDimensions01;
internal void ConfigureAvailableCustomDimensions01(params CustomDimensions01[] customDimensions)
=> configureAvailableCustomDimensions01(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string> setCustomDimension02;
internal void SetCustomDimension02(string dimension02)
=> setCustomDimension02(dimension02);
private readonly Action<string[]> configureAvailableCustomDimensions02;
internal void ConfigureAvailableCustomDimensions02(params CustomDimensions02[] customDimensions)
=> configureAvailableCustomDimensions02(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string[]> configureAvailableResourceCurrencies;
internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions)
=> configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string[]> configureAvailableCustomDimensions03;
internal void ConfigureAvailableCustomDimensions03(params CustomDimensions03[] customDimensions)
=> configureAvailableCustomDimensions03(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string> setCustomDimension03;
internal void SetCustomDimension03(string dimension03)
=> setCustomDimension03(dimension03);
private readonly Action<string[]> configureAvailableResourceItemTypes;
internal void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes)
=> configureAvailableResourceItemTypes(resourceItemTypes);
private readonly Action<bool> setEnabledInfoLog;
internal void SetEnabledInfoLog(bool enabled)
=> setEnabledInfoLog(enabled);
private readonly Action<bool> setEnabledVerboseLog;
internal void SetEnabledVerboseLog(bool enabled)
=> setEnabledVerboseLog(enabled);
#endregion
#region Data required to fetch methods via reflection
private const string AssemblyName = "GameAnalytics.NetStandard";
private const string Namespace = "GameAnalyticsSDK.Net";
private const string MainClass = "GameAnalytics";
private const string EnumPrefix = "EGA";
#endregion
#region Call implementations
private readonly object?[] args1 = new object?[1];
private readonly object?[] args2 = new object?[2];
private readonly object?[] args3 = new object?[3];
private readonly object?[] args4 = new object?[4];
private readonly object?[] args5 = new object?[5];
private Action Call(MethodInfo methodInfo)
=> () => methodInfo?.Invoke(null, null);
private Action<T> Call<T>(MethodInfo methodInfo)
=> (T arg1) =>
{
args1[0] = arg1;
methodInfo.Invoke(null, args1);
};
private Action<T1, T2> Call<T1, T2>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2) =>
{
args2[0] = arg1;
args2[1] = arg2;
methodInfo.Invoke(null, args2);
};
private Action<T1, T2, T3> Call<T1, T2, T3>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2, T3 arg3) =>
{
args3[0] = arg1;
args3[1] = arg2;
args3[2] = arg3;
methodInfo.Invoke(null, args3);
};
private Action<T1, T2, T3, T4> Call<T1, T2, T3, T4>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2, T3 arg3, T4 arg4) =>
{
args4[0] = arg1;
args4[1] = arg2;
args4[2] = arg3;
args4[3] = arg4;
methodInfo.Invoke(null, args4);
};
private Action<T1, T2, T3, T4, T5> Call<T1, T2, T3, T4, T5>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) =>
{
args5[0] = arg1;
args5[1] = arg2;
args5[2] = arg3;
args5[3] = arg4;
args5[4] = arg5;
methodInfo.Invoke(null, args5);
};
#endregion
private AssemblyLoadContext? loadContext;
private Assembly? assembly;
private string GetAssemblyPath(string assemblyName)
=> Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
$"{assemblyName}.dll");
private bool resolvingDependency;
private Assembly? ResolveDependency(AssemblyLoadContext context, AssemblyName dependencyName)
{
if (resolvingDependency) { return null; }
resolvingDependency = true;
Assembly dependency = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null")));
resolvingDependency = false;
return dependency;
}
internal Implementation()
{
loadContext = new AssemblyLoadContext(AssemblyName, isCollectible: true);
loadContext.Resolving += ResolveDependency;
assembly = loadContext.LoadFromAssemblyPath(
GetAssemblyPath(AssemblyName));
Type getType(string name)
=> assembly.GetType($"{Namespace}.{name}")
?? throw new Exception($"Could not find type\"{Namespace}.{name}\"");
var mainClass = getType(MainClass);
var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}");
var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}");
var resourceFlowTypeEnumType = getType($"{EnumPrefix}{nameof(ResourceFlowType)}");
MethodInfo getMethod(string name, Type[] types)
{
foreach (var me in mainClass.GetMethods())
{
var aksjdnakjsdnf = me;
}
return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null)
?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}");
}
initialize = Call<string, string>(getMethod(nameof(Initialize),
new Type[] { typeof(string), typeof(string) }));
configureBuild = Call<string>(getMethod(nameof(ConfigureBuild),
new Type[] { typeof(string) }));
addErrorEvent = Call<ErrorSeverity, string>(getMethod(nameof(AddErrorEvent),
new Type[] { errorSeverityEnumType, typeof(string) }));
addDesignEvent0 = Call<string, IDictionary<string, object>?>(getMethod(nameof(AddDesignEvent),
new Type[] { typeof(string), typeof(IDictionary<string, object>) }));
addDesignEvent1 = Call<string, double>(getMethod(nameof(AddDesignEvent),
new Type[] { typeof(string), typeof(double) }));
addProgressionEvent01 = Call<ProgressionStatus, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string) }));
addProgressionEvent01Score = Call<ProgressionStatus, string, double>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(double) }));
addProgressionEvent02 = Call<ProgressionStatus, string, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(string) }));
addProgressionEvent03 = Call<ProgressionStatus, string, string, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) }));
setCustomDimension01 = Call<string>(getMethod(nameof(SetCustomDimension01),
new Type[] { typeof(string) }));
configureAvailableCustomDimensions01 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions01),
new Type[] { typeof(string[]) }));
setCustomDimension02 = Call<string>(getMethod(nameof(SetCustomDimension02),
new Type[] { typeof(string) }));
configureAvailableCustomDimensions02 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions02),
new Type[] { typeof(string[]) }));
configureAvailableCustomDimensions03 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions03),
new Type[] { typeof(string[]) }));
setCustomDimension03 = Call<string>(getMethod(nameof(SetCustomDimension03),
new Type[] { typeof(string) }));
configureAvailableResourceCurrencies = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceCurrencies),
new Type[] { typeof(string[]) }));
configureAvailableResourceItemTypes = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceItemTypes),
new Type[] { typeof(string[]) }));
addResourceEvent = Call<ResourceFlowType, string, float, string, string>(getMethod(nameof(AddResourceEvent),
new Type[] { resourceFlowTypeEnumType, typeof(string), typeof(float), typeof(string), typeof(string) }));
setEnabledInfoLog = Call<bool>(getMethod(nameof(SetEnabledInfoLog),
new Type[] { typeof(bool) }));
setEnabledVerboseLog = Call<bool>(getMethod(nameof(SetEnabledVerboseLog),
new Type[] { typeof(bool) }));
onQuit = Call(getMethod("OnQuit", Array.Empty<Type>()));
}
private readonly Action? onQuit;
private void OnQuit()
{
try
{
if (assembly != null) { onQuit?.Invoke(); }
}
catch (Exception e)
{
e = e.GetInnermost();
DebugConsole.AddWarning($"Failed to call GameAnalytics.OnQuit: {e.Message} {e.StackTrace}");
//If this happens then GameAnalytics is just broken,
//let's just hope that it uninitialized correctly and
//allow the game to keep running
}
}
public void Dispose()
{
if (loadContext is null) { return; }
OnQuit();
loadContext?.Unload();
loadContext = null;
assembly = null;
}
}
private static Implementation? loadedImplementation;
private static void ValidateEventID(string eventID)
{
#if DEBUG
string[] parts = eventID.Split(':');
if (parts.Length > 5)
{
DebugConsole.ThrowError($"Invalid GameAnalytics event id \"{eventID}\". Only 5 id parts allowed separated by ':'");
}
if (parts.Any(p => p.Length > 32))
{
DebugConsole.ThrowError($"Invalid GameAnalytics event id \"{eventID}\". Each id part separated by ':' must be 32 characters or less.");
}
#endif
}
public enum DataSampleSize
{
Small,
Medium,
Large,
Full
}
private readonly static Dictionary<DataSampleSize, float> dataSampleSizes = new Dictionary<DataSampleSize, float>()
{
{ DataSampleSize.Small, 0.01f },
{ DataSampleSize.Medium, 0.05f },
{ DataSampleSize.Large, 0.5f },
{ DataSampleSize.Full, 1.0f }
};
/// <summary>
/// Should we log something into GameAnalytics if we only want a random sample of some events?
/// Essentially just randomly decides whether to log or not based on the probability
/// </summary>
/// <param name="probability">A value between 0 and 1</param>
/// <returns></returns>
public static bool ShouldLogRandomSample(DataSampleSize sampleSize = DataSampleSize.Small)
{
return Rand.Range(0.0f, 1.0f) < dataSampleSizes[sampleSize];
}
public static void AddErrorEvent(ErrorSeverity errorSeverity, string message)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddErrorEvent(errorSeverity, message);
}
/// <summary>
/// Adds an error event to GameAnalytics if an event with the same identifier has not been added yet.
/// </summary>
public static void AddErrorEventOnce(string identifier, ErrorSeverity errorSeverity, string message)
{
if (!SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (ContentPackageManager.ModsEnabled)
{
message = "[MODDED] " + message;
}
loadedImplementation?.AddErrorEvent(errorSeverity, message);
sentEventIdentifiers.Add(identifier);
}
public static void AddDesignEvent(string eventID)
{
if (!SendUserStatistics) { return; }
ValidateEventID(eventID);
loadedImplementation?.AddDesignEvent(eventID);
}
public static void AddDesignEvent(string eventID, double value)
{
if (!SendUserStatistics) { return; }
ValidateEventID(eventID);
loadedImplementation?.AddDesignEvent(eventID, value);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, double score)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, score);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02);
}
public static void AddProgressionEvent(ProgressionStatus progressionStatus, string progression01, string progression02, string progression03)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03);
}
public static void SetCustomDimension01(CustomDimensions01 dimension)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.SetCustomDimension01(dimension.ToString());
}
public static void SetCustomDimension03(CustomDimensions03 dimension)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.SetCustomDimension03(dimension.ToString());
}
public static void SetCurrentLevel(LevelData levelData)
{
if (!SendUserStatistics) { return; }
CustomDimensions02 customDimension = CustomDimensions02.None;
if (levelData != null)
{
float levelDifficulty = levelData.Difficulty;
customDimension = (CustomDimensions02)MathHelper.Clamp((int)(levelDifficulty / 10) + 1, 0, Enum.GetValues(typeof(CustomDimensions02)).Length - 1);
}
loadedImplementation?.SetCustomDimension02(customDimension.ToString());
}
public static void AddMoneyGainedEvent(int amount, MoneySource moneySource, string eventId)
{
AddResourceEvent(ResourceFlowType.Source, ResourceCurrency.Money, amount, moneySource.ToString(), eventId);
}
public static void AddMoneySpentEvent(int amount, MoneySink moneySink, string eventId)
{
AddResourceEvent(ResourceFlowType.Sink, ResourceCurrency.Money, amount, moneySink.ToString(), eventId);
}
private static void AddResourceEvent(ResourceFlowType flowType, ResourceCurrency currency, float amount, string eventType, string eventId)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddResourceEvent(flowType, currency.ToString(), amount, eventType, eventId);
}
private static void Init()
{
ShutDown();
try
{
loadedImplementation = new Implementation();
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
SetConsent(Consent.Error);
return;
}
#if DEBUG
try
{
loadedImplementation?.SetEnabledInfoLog(true);
loadedImplementation?.SetEnabledVerboseLog(true);
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
SetConsent(Consent.Error);
return;
}
#endif
string exePath = Assembly.GetEntryAssembly()!.Location;
string? exeName = string.Empty;
#if SERVER
exeName = "s";
#endif
Md5Hash? exeHash = null;
try
{
exeHash = Md5Hash.CalculateForFile(exePath, Md5Hash.StringHashOptions.BytePerfect);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error while calculating MD5 hash for the executable \"" + exePath + "\"", e);
}
try
{
string buildConfiguration = "Release";
#if DEBUG
buildConfiguration = "Debug";
#elif UNSTABLE
buildConfiguration = "Unstable";
#endif
loadedImplementation?.ConfigureBuild(GameMain.Version.ToString()
+ exeName + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast<CustomDimensions01>().ToArray());
loadedImplementation?.ConfigureAvailableCustomDimensions02(Enum.GetValues(typeof(CustomDimensions02)).Cast<CustomDimensions02>().ToArray());
loadedImplementation?.ConfigureAvailableCustomDimensions03(Enum.GetValues(typeof(CustomDimensions03)).Cast<CustomDimensions03>().ToArray());
loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast<ResourceCurrency>().ToArray());
loadedImplementation?.ConfigureAvailableResourceItemTypes(
Enum.GetValues(typeof(MoneySink)).Cast<MoneySink>().Select(s => s.ToString()).Union(Enum.GetValues(typeof(MoneySource)).Cast<MoneySource>().Select(s => s.ToString())).ToArray());
InitKeys();
loadedImplementation?.AddDesignEvent("Executable:"
+ GameMain.Version.ToString()
+ exeName + ":"
+ (exeHash?.ShortRepresentation ?? "Unknown") + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
SetCustomDimension01(ContentPackageManager.ModsEnabled ? CustomDimensions01.Modded : CustomDimensions01.Vanilla);
CustomDimensions03 platform = CustomDimensions03.UnknownPlatform;
if (SteamManager.IsInitialized)
{
platform = CustomDimensions03.Steam;
}
else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
{
platform = CustomDimensions03.EGS;
}
SetCustomDimension03(platform);
}
catch (Exception e)
{
DebugConsole.ThrowError("Initializing GameAnalytics failed. Disabling user statistics...", e);
SetConsent(Consent.Error);
return;
}
var allPackages = ContentPackageManager.EnabledPackages.All.ToList();
if (allPackages?.Count > 0)
{
List<string> packageNames = new List<string>();
foreach (ContentPackage cp in allPackages)
{
string sanitizedName = cp.Name.Replace(":", "").Replace(" ", "");
sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length));
packageNames.Add(sanitizedName);
loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName);
}
packageNames.Sort();
loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames));
}
loadedImplementation?.AddDesignEvent("Language:" + GameSettings.CurrentConfig.Language);
}
static partial void InitKeys();
public static void ShutDown()
{
loadedImplementation?.Dispose();
loadedImplementation = null;
}
}
}