Decompiled source of GsValheimStatsClientFix v1.0.0

GsValheimStatsClientFix.dll

Decompiled 4 days ago
using 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
			{
			}
		}
	}
}