Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of GsValheimStatsClientFix v1.0.0
GsValheimStatsClientFix.dll
Decompiled 4 days agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("GsValheimStatsClientFix")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+70d8aa199cc82137ecc8d1108cbfcc5c67b6346a")] [assembly: AssemblyProduct("GsValheimStatsClientFix")] [assembly: AssemblyTitle("GsValheimStatsClientFix")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace GsValheimStatsClientFix { internal static class ClientTargets { internal const string ClientTypeName = "GsValheimStatsClient.Plugin"; internal static MethodInfo ReadProfile { get; private set; } internal static MethodInfo ReadStat { get; private set; } internal static MethodInfo LoadWeapons { get; private set; } internal static FieldInfo LifeActive { get; private set; } internal static FieldInfo LifeStartKills { get; private set; } internal static FieldInfo LifeStartPlaySec { get; private set; } internal static bool Resolve(ManualLogSource logger) { Type type = AccessTools.TypeByName("GsValheimStatsClient.Plugin"); if (type == null) { logger.LogError((object)"Could not resolve type 'GsValheimStatsClient.Plugin'. Nothing patched."); return false; } ReadProfile = AccessTools.Method(type, "ReadProfile", new Type[4] { typeof(Dictionary<string, long>), typeof(Dictionary<string, int>), typeof(Dictionary<string, int>), typeof(Dictionary<string, int>) }, (Type[])null); ReadStat = AccessTools.Method(type, "ReadStat", new Type[1] { typeof(string) }, (Type[])null); LoadWeapons = AccessTools.Method(type, "LoadWeapons", Type.EmptyTypes, (Type[])null); LifeActive = AccessTools.Field(type, "lifeActive"); LifeStartKills = AccessTools.Field(type, "lifeStartKills"); LifeStartPlaySec = AccessTools.Field(type, "lifeStartPlaySec"); List<string> list = new List<string>(); if (ReadProfile == null) { list.Add("GsValheimStatsClient.Plugin.ReadProfile(Dictionary<string,long>, Dictionary<string,int>, Dictionary<string,int>, Dictionary<string,int>)"); } if (ReadStat == null) { list.Add("GsValheimStatsClient.Plugin.ReadStat(string)"); } if (LoadWeapons == null) { list.Add("GsValheimStatsClient.Plugin.LoadWeapons()"); } if (LifeActive == null) { list.Add("GsValheimStatsClient.Plugin.lifeActive"); } if (LifeStartKills == null) { list.Add("GsValheimStatsClient.Plugin.lifeStartKills"); } if (LifeStartPlaySec == null) { list.Add("GsValheimStatsClient.Plugin.lifeStartPlaySec"); } if (list.Count > 0) { foreach (string item in list) { logger.LogError((object)("Could not resolve " + item + ".")); } logger.LogError((object)("Nothing patched (" + list.Count + " target(s) unresolved). GsValheimStatsClient is left exactly as it was.")); return false; } return true; } } internal static class GameStructureCheck { internal static bool Verify(ManualLogSource logger) { List<string> list = new List<string>(); VerifyGame(list); VerifyProfile(list); VerifyStatTypeEnum(list); if (list.Count > 0) { foreach (string item in list) { logger.LogError((object)("Valheim structure check failed: " + item + ".")); } logger.LogError((object)("Nothing patched (" + list.Count + " Valheim member(s) missing or changed shape). This build of Valheim is not the one this patch was written against.")); return false; } return true; } private static void VerifyGame(List<string> missing) { Type type = AccessTools.TypeByName("Game"); if (type == null) { missing.Add("type Game not found"); return; } if (AccessTools.Property(type, "instance") == null) { missing.Add("Game.instance not found"); } if (AccessTools.Method(type, "GetPlayerProfile", Type.EmptyTypes, (Type[])null) == null) { missing.Add("Game.GetPlayerProfile() not found"); } } private static void VerifyProfile(List<string> missing) { Type type = AccessTools.TypeByName("PlayerProfile"); if (type == null) { missing.Add("type PlayerProfile not found"); return; } FieldInfo fieldInfo = AccessTools.Field(type, "m_playerStats"); if (fieldInfo == null) { missing.Add("PlayerProfile.m_playerStats not found"); return; } if (!fieldInfo.FieldType.IsArray) { missing.Add("PlayerProfile.m_playerStats is " + fieldInfo.FieldType.Name + ", expected an array"); return; } Type elementType = fieldInfo.FieldType.GetElementType(); if (elementType == null) { missing.Add("PlayerProfile.m_playerStats has no element type"); return; } FieldInfo fieldInfo2 = AccessTools.Field(elementType, "m_enemyStats"); if (fieldInfo2 == null) { missing.Add(elementType.Name + ".m_enemyStats not found"); } else if (!fieldInfo2.FieldType.IsArray) { missing.Add(elementType.Name + ".m_enemyStats is " + fieldInfo2.FieldType.Name + ", expected an array"); } RequireField(missing, elementType, "m_stats"); RequireField(missing, elementType, "m_itemCraftStats"); RequireField(missing, elementType, "m_itemPickupStats"); } private static void VerifyStatTypeEnum(List<string> missing) { Type type = AccessTools.TypeByName("PlayerStatType"); if (type == null) { missing.Add("type PlayerStatType not found"); return; } if (!type.IsEnum) { missing.Add("PlayerStatType is not an enum"); return; } string[] array = new string[4] { "Deaths", "EnemyKills", "TimeInBase", "TimeOutOfBase" }; foreach (string text in array) { if (!Enum.IsDefined(type, text)) { missing.Add("PlayerStatType." + text + " not defined"); } } } private static void RequireField(List<string> missing, Type owner, string fieldName) { if (AccessTools.Field(owner, fieldName) == null) { missing.Add(owner.Name + "." + fieldName + " not found"); } } } internal static class GameVersionProbe { internal static string DescribeOrUnknown() { try { string text = ReadCurrentVersion(); return string.IsNullOrEmpty(text) ? "unknown" : text; } catch { return "unknown"; } } [MethodImpl(MethodImplOptions.NoInlining)] private static string ReadCurrentVersion() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) return ((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString(); } } internal static class LifeBaselinePatch { internal static ManualLogSource Log; private static bool reported; internal static void LoadWeaponsPostfix(object __instance) { try { if (__instance == null) { Report(warning: true, "life baseline check skipped: Harmony passed a null instance."); return; } object value = ClientTargets.LifeActive.GetValue(__instance); if (!(value is bool)) { Report(warning: true, "life baseline check skipped: lifeActive was not a bool."); return; } if (!(bool)value) { Report(warning: false, "life baseline check: no active life was loaded, nothing to repair (a character's first login, or a life that had already ended)."); return; } object value2 = ClientTargets.LifeStartKills.GetValue(__instance); object value3 = ClientTargets.LifeStartPlaySec.GetValue(__instance); if (!(value2 is double) || !(value3 is double)) { Report(warning: true, "life baseline check skipped: lifeStartKills/lifeStartPlaySec were not doubles."); return; } double num = (double)value2; double num2 = (double)value3; if (num != 0.0 || num2 != 0.0) { Report(warning: false, "life baseline left alone: a life is in progress with a non-zero baseline (lifeStartKills=" + Num(num) + ", lifeStartPlaySec=" + Num(num2) + ")."); return; } double deaths; string text = ProfilePatches.TryReadDeaths(out deaths); if (text != null) { Report(warning: true, "life baseline repair skipped: could not read the death count (" + text + "). The loaded baseline (lifeStartKills=" + Num(num) + ", lifeStartPlaySec=" + Num(num2) + ") is left as it was, because wrongly resetting a real life cannot be undone. This retries next session."); } else if (deaths <= 0.0) { Report(warning: false, "life baseline left alone: baseline is zero (lifeStartKills=" + Num(num) + ", lifeStartPlaySec=" + Num(num2) + ") but this character has never died (Deaths=" + Num(deaths) + "), so that zero is the true start of its one continuous life."); } else { ClientTargets.LifeActive.SetValue(__instance, false); Report(warning: false, "life baseline reset: baseline was zero (lifeStartKills=" + Num(num) + ", lifeStartPlaySec=" + Num(num2) + ") on a character that has died (Deaths=" + Num(deaths) + "), so it was saved while the profile reads were broken. GsValheimStatsClient will re-baseline this life from the corrected reads on its next update, so the current life starts from now instead of from the beginning of the character's history."); } } catch (Exception ex) { Report(warning: true, "life baseline check failed (" + ex.GetType().Name + ": " + ex.Message + "). The client's loaded baseline is left as it was."); } } private static string Num(double value) { try { return value.ToString("0.###", CultureInfo.InvariantCulture); } catch { return "?"; } } private static void Report(bool warning, string message) { try { if (reported) { return; } reported = true; ManualLogSource log = Log; if (log != null) { if (warning) { log.LogWarning((object)message); } else { log.LogInfo((object)message); } } } catch { } } } [BepInPlugin("com.gsvalheimstatsclientfix.valheim", "GsValheimStatsClientFix", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.gsvalheimstatsclientfix.valheim"; public const string PluginName = "GsValheimStatsClientFix"; public const string PluginVersion = "1.0.0"; public const string ClientGuid = "net.cproudlock.gsvalheimstatsclient"; public const string SupportedClientVersion = "0.2.12"; private void Awake() { string loadedClientVersion = GetLoadedClientVersion(); if (loadedClientVersion == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"GsValheimStatsClientFix 1.0.0: GsValheimStatsClient (net.cproudlock.gsvalheimstatsclient) is not loaded. Nothing patched."); } else if (loadedClientVersion != "0.2.12") { ((BaseUnityPlugin)this).Logger.LogWarning((object)("GsValheimStatsClientFix 1.0.0: found GsValheimStatsClient " + loadedClientVersion + ", but this patch only supports 0.2.12. Nothing patched.")); } else if (GameStructureCheck.Verify(((BaseUnityPlugin)this).Logger) && ClientTargets.Resolve(((BaseUnityPlugin)this).Logger)) { ProfilePatches.Log = ((BaseUnityPlugin)this).Logger; LifeBaselinePatch.Log = ((BaseUnityPlugin)this).Logger; if (ApplyPatches()) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("GsValheimStatsClientFix 1.0.0 patched GsValheimStatsClient " + loadedClientVersion + " for Valheim " + GameVersionProbe.DescribeOrUnknown() + ".")); } } } private bool ApplyPatches() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Expected O, but got Unknown //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Expected O, but got Unknown Harmony val = new Harmony("com.gsvalheimstatsclientfix.valheim"); string text = null; try { text = "GsValheimStatsClient.Plugin.ReadProfile"; val.Patch((MethodBase)ClientTargets.ReadProfile, new HarmonyMethod(AccessTools.Method(typeof(ProfilePatches), "ReadProfilePrefix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); text = "GsValheimStatsClient.Plugin.ReadStat"; val.Patch((MethodBase)ClientTargets.ReadStat, new HarmonyMethod(AccessTools.Method(typeof(ProfilePatches), "ReadStatPrefix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); text = "GsValheimStatsClient.Plugin.LoadWeapons"; val.Patch((MethodBase)ClientTargets.LoadWeapons, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(LifeBaselinePatch), "LoadWeaponsPostfix", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Failed to patch " + text + ": " + ex)); try { val.UnpatchSelf(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)("Rollback of com.gsvalheimstatsclientfix.valheim also failed: " + ex2)); } ((BaseUnityPlugin)this).Logger.LogError((object)"Nothing is patched. GsValheimStatsClient is left exactly as it was."); return false; } } private static string GetLoadedClientVersion() { if (!Chainloader.PluginInfos.TryGetValue("net.cproudlock.gsvalheimstatsclient", out var value) || value == null || value.Metadata == null) { return null; } if (!(value.Metadata.Version == null)) { return value.Metadata.Version.ToString(); } return null; } } internal static class ProfilePatches { internal static ManualLogSource Log; private static bool warnedReadProfile; private static bool warnedReadStat; private static bool loggedFirstRead; internal static bool ReadProfilePrefix(Dictionary<string, long> statsKv, Dictionary<string, int> enemyKills, Dictionary<string, int> craftStats, Dictionary<string, int> pickups) { try { if (statsKv == null || enemyKills == null || craftStats == null || pickups == null) { return WarnReadProfile("the client passed a null dictionary"); } string text = ReadProfileInner(statsKv, enemyKills, craftStats, pickups); if (text != null) { return WarnReadProfile(text); } return false; } catch (Exception ex) { return WarnReadProfile("threw " + ex.GetType().Name + ": " + ex.Message); } } internal static bool ReadStatPrefix(string statName, ref double __result) { try { if (string.IsNullOrEmpty(statName)) { return WarnReadStat("the client passed a null or empty stat name"); } double value; string text = ReadStatInner(statName, out value); if (text != null) { return WarnReadStat(text); } __result = value; return false; } catch (Exception ex) { return WarnReadStat("threw " + ex.GetType().Name + ": " + ex.Message); } } [MethodImpl(MethodImplOptions.NoInlining)] private static string ReadProfileInner(Dictionary<string, long> statsKv, Dictionary<string, int> enemyKills, Dictionary<string, int> craftStats, Dictionary<string, int> pickups) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) string failure; PlayerStats rawStats = GetRawStats(out failure); if (rawStats == null) { return failure; } Dictionary<PlayerStatType, float> stats = rawStats.m_stats; if (stats == null) { return "PlayerStats.m_stats was null"; } Dictionary<string, float>[] enemyStats = rawStats.m_enemyStats; if (enemyStats == null || enemyStats.Length == 0) { return "PlayerStats.m_enemyStats was null or empty"; } Dictionary<string, float> dictionary = enemyStats[0]; if (dictionary == null) { return "PlayerStats.m_enemyStats[0] was null"; } Dictionary<string, float> itemCraftStats = rawStats.m_itemCraftStats; if (itemCraftStats == null) { return "PlayerStats.m_itemCraftStats was null"; } Dictionary<string, float> itemPickupStats = rawStats.m_itemPickupStats; if (itemPickupStats == null) { return "PlayerStats.m_itemPickupStats was null"; } Dictionary<string, long> dictionary2 = new Dictionary<string, long>(); Dictionary<string, int> dictionary3 = new Dictionary<string, int>(); Dictionary<string, int> dictionary4 = new Dictionary<string, int>(); Dictionary<string, int> dictionary5 = new Dictionary<string, int>(); foreach (KeyValuePair<PlayerStatType, float> item in stats) { double num = item.Value; if (num > 0.0) { dictionary2["vh_" + ((object)item.Key/*cast due to .constrained prefix*/).ToString()] = (long)num; } } FillCountDictionary(dictionary, dictionary3); FillCountDictionary(itemCraftStats, dictionary4); FillCountDictionary(itemPickupStats, dictionary5); foreach (KeyValuePair<string, long> item2 in dictionary2) { statsKv[item2.Key] = item2.Value; } CopyInto(dictionary3, enemyKills); CopyInto(dictionary4, craftStats); CopyInto(dictionary5, pickups); if (!loggedFirstRead) { loggedFirstRead = true; stats.TryGetValue((PlayerStatType)0, out var value); LogSafe(warning: false, "first successful profile read: " + dictionary2.Count + " stats, " + dictionary3.Count + " creatures, " + dictionary4.Count + " crafts, " + dictionary5.Count + " pickups, Deaths=" + (long)value + "."); } return null; } [MethodImpl(MethodImplOptions.NoInlining)] private unsafe static string ReadStatInner(string statName, out double value) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) value = 0.0; string failure; PlayerStats rawStats = GetRawStats(out failure); if (rawStats == null) { return failure; } Dictionary<PlayerStatType, float> stats = rawStats.m_stats; if (stats == null) { return "PlayerStats.m_stats was null"; } if (!Enum.TryParse<PlayerStatType>(statName, out PlayerStatType result) || ((object)(*(PlayerStatType*)(&result))/*cast due to .constrained prefix*/).ToString() != statName) { return null; } if (stats.TryGetValue(result, out var value2)) { value = value2; } return null; } [MethodImpl(MethodImplOptions.NoInlining)] internal static string TryReadDeaths(out double deaths) { deaths = 0.0; string failure; PlayerStats rawStats = GetRawStats(out failure); if (rawStats == null) { return failure; } Dictionary<PlayerStatType, float> stats = rawStats.m_stats; if (stats == null) { return "PlayerStats.m_stats was null"; } if (stats.TryGetValue((PlayerStatType)0, out var value)) { deaths = value; } return null; } [MethodImpl(MethodImplOptions.NoInlining)] private static PlayerStats GetRawStats(out string failure) { Game instance = Game.instance; if ((Object)(object)instance == (Object)null) { failure = "Game.instance was null"; return null; } PlayerProfile playerProfile = instance.GetPlayerProfile(); if (playerProfile == null) { failure = "Game.instance.GetPlayerProfile() returned null"; return null; } PlayerStats[] playerStats = playerProfile.m_playerStats; if (playerStats == null || playerStats.Length == 0) { failure = "PlayerProfile.m_playerStats was null or empty"; return null; } PlayerStats val = playerStats[0]; if (val == null) { failure = "PlayerProfile.m_playerStats[0] was null"; return null; } failure = null; return val; } private static void FillCountDictionary(Dictionary<string, float> source, Dictionary<string, int> destination) { foreach (KeyValuePair<string, float> item in source) { int num = (int)(double)item.Value; if (num > 0) { destination[item.Key] = num; } } } private static void CopyInto(Dictionary<string, int> source, Dictionary<string, int> destination) { foreach (KeyValuePair<string, int> item in source) { destination[item.Key] = item.Value; } } private static bool WarnReadProfile(string reason) { if (!warnedReadProfile) { warnedReadProfile = true; LogSafe(warning: true, "ReadProfile patch stood down (" + reason + "). The client's original code will run instead for the rest of this session, so profile-derived fields stay at their pre-patch values. This is logged once per session."); } return true; } private static bool WarnReadStat(string reason) { if (!warnedReadStat) { warnedReadStat = true; LogSafe(warning: true, "ReadStat patch stood down (" + reason + "). The client's original code will run instead for the rest of this session. This is logged once per session."); } return true; } private static void LogSafe(bool warning, string message) { try { ManualLogSource log = Log; if (log != null) { if (warning) { log.LogWarning((object)message); } else { log.LogInfo((object)message); } } } catch { } } } }