Decompiled source of EilifCompanionClient v0.2.0

EilifCompanionClient.dll

Decompiled 7 hours ago
using System;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
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.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("EilifCompanionClient")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+5109d837318fa85e77890bf184d65a8f6bb60fec")]
[assembly: AssemblyProduct("EilifCompanionClient")]
[assembly: AssemblyTitle("EilifCompanionClient")]
[assembly: AssemblyVersion("0.2.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 EilifCompanionClient
{
	internal static class EilifDeathReporter
	{
		private static readonly FieldInfo LastHitField = AccessTools.Field(typeof(Character), "m_lastHit");

		internal static void Report(Player who)
		{
			//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_016f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0188: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)who == (Object)null || (Object)(object)who != (Object)(object)Player.m_localPlayer || !EilifMapTrackerPlugin.IsOnServer())
			{
				return;
			}
			if (LastHitField == null)
			{
				ManualLogSource log = EilifMapTrackerPlugin.Log;
				if (log != null)
				{
					log.LogWarning((object)"[EilifDeath] Character.m_lastHit not found — the game's field layout changed; no death reported.");
				}
				return;
			}
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			object? value = LastHitField.GetValue(who);
			HitData val = (HitData)((value is HitData) ? value : null);
			if (val == null)
			{
				ManualLogSource log2 = EilifMapTrackerPlugin.Log;
				if (log2 != null)
				{
					log2.LogWarning((object)"[EilifDeath] no m_lastHit at death — nothing to report.");
				}
				return;
			}
			string text = ((object)Unsafe.As<HitType, HitType>(ref val.m_hitType)/*cast due to .constrained prefix*/).ToString();
			string text2 = ResolveAttacker(val);
			string text3 = ResolveBiome(who);
			string playerName = who.GetPlayerName();
			string text4 = instance.GetWorldName() ?? "";
			if (!string.IsNullOrEmpty(playerName))
			{
				Vector3 position = ((Component)who).transform.position;
				string s = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
				string json = "{\"schemaVersion\":1,\"game\":\"valheim\",\"source\":\"eilif-death\",\"world\":" + EilifMapTrackerPlugin.JsonStr(text4) + ",\"player\":" + EilifMapTrackerPlugin.JsonStr(playerName) + ",\"tsUtc\":" + EilifMapTrackerPlugin.JsonStr(s) + ",\"hitType\":" + EilifMapTrackerPlugin.JsonStr(text) + ",\"attacker\":" + ((text2 == null) ? "null" : EilifMapTrackerPlugin.JsonStr(text2)) + ",\"biome\":" + EilifMapTrackerPlugin.JsonStr(text3) + ",\"pos\":{\"x\":" + Fmt(position.x) + ",\"z\":" + Fmt(position.z) + "}}";
				ManualLogSource log3 = EilifMapTrackerPlugin.Log;
				if (log3 != null)
				{
					log3.LogInfo((object)("[EilifDeath] " + playerName + " died in " + text4 + ": hitType=" + text + ", attacker=" + (text2 ?? "(none)") + ", biome=" + text3));
				}
				EilifMapTrackerPlugin.PostJson(json, "[EilifDeath]", "[EilifDeath] reported " + text + ((text2 == null) ? "" : (" by " + text2)) + " for " + playerName);
			}
		}

		private static string ResolveAttacker(HitData hit)
		{
			try
			{
				Character attacker = hit.GetAttacker();
				if ((Object)(object)attacker == (Object)null)
				{
					return null;
				}
				Player val = (Player)(object)((attacker is Player) ? attacker : null);
				if ((Object)(object)val != (Object)null)
				{
					string playerName = val.GetPlayerName();
					return string.IsNullOrEmpty(playerName) ? null : playerName;
				}
				string name = attacker.m_name;
				if (!string.IsNullOrEmpty(name))
				{
					return name;
				}
				return PrefabName(((Component)attacker).gameObject);
			}
			catch (Exception ex)
			{
				ManualLogSource log = EilifMapTrackerPlugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("[EilifDeath] attacker lookup failed: " + ex.Message));
				}
				return null;
			}
		}

		private unsafe static string ResolveBiome(Player who)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				Vector3 position = ((Component)who).transform.position;
				Biome val = Heightmap.FindBiome(position);
				if ((int)val == 0 && WorldGenerator.instance != null)
				{
					val = WorldGenerator.instance.GetBiome(position.x, position.z, 0.02f, false);
				}
				return ((object)(*(Biome*)(&val))/*cast due to .constrained prefix*/).ToString();
			}
			catch
			{
				return "None";
			}
		}

		private static string PrefabName(GameObject go)
		{
			if ((Object)(object)go == (Object)null)
			{
				return null;
			}
			string name = ((Object)go).name;
			if (string.IsNullOrEmpty(name))
			{
				return null;
			}
			int num = name.IndexOf("(Clone)", StringComparison.OrdinalIgnoreCase);
			if (num <= 0)
			{
				return name;
			}
			return name.Substring(0, num);
		}

		private static string Fmt(float v)
		{
			return ((float)Math.Round(v, 1)).ToString("0.#", CultureInfo.InvariantCulture);
		}
	}
	[HarmonyPatch(typeof(Player), "OnDeath")]
	internal static class Patch_PlayerOnDeath
	{
		private static void Postfix(Player __instance)
		{
			try
			{
				EilifDeathReporter.Report(__instance);
			}
			catch (Exception ex)
			{
				ManualLogSource log = EilifMapTrackerPlugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("[EilifDeath] death hook failed: " + ex.Message));
				}
			}
		}
	}
	[BepInPlugin("net.eilif.companionclient", "Eilif Companion Client", "0.2.0")]
	public class EilifMapTrackerPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "net.eilif.companionclient";

		public const string PluginName = "Eilif Companion Client";

		public const string PluginVersion = "0.2.0";

		internal static ManualLogSource Log;

		internal static EilifMapTrackerPlugin Instance;

		internal static string IngestUrl = "";

		internal static string IngestToken = "";

		private ConfigEntry<string> _url;

		private ConfigEntry<string> _token;

		private ConfigEntry<int> _intervalSeconds;

		private static readonly HttpClient Http = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(20.0)
		};

		private int _postInFlight;

		private float _postTimer;

		private bool _wasConnected;

		private float _lastPct = -1f;

		private static readonly FieldInfo ExploredField = AccessTools.Field(typeof(Minimap), "m_explored");

		private static readonly FieldInfo TextureSizeField = AccessTools.Field(typeof(Minimap), "m_textureSize");

		private string _lastCachedPlayer = "";

		private string _lastCachedWorld = "";

		private void Awake()
		{
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Expected O, but got Unknown
			//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			Instance = this;
			_url = ((BaseUnityPlugin)this).Config.Bind<string>("Map", "Url", "https://valheim-dashboard.vercel.app/api/gs-ingest", "Dashboard ingest endpoint. The explored-map % is POSTed here as source:'client-map'.");
			_token = ((BaseUnityPlugin)this).Config.Bind<string>("Map", "Token", "", "Optional Bearer token. If set it is sent as 'Authorization: Bearer <token>'. Blank is fine (pilot).");
			_intervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Map", "IntervalSeconds", 300, new ConfigDescription("Seconds between map-% posts while connected to a server.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(60, 3600), Array.Empty<object>()));
			IngestUrl = _url.Value;
			IngestToken = _token.Value;
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			}
			catch
			{
			}
			Harmony val = new Harmony("net.eilif.companionclient");
			val.PatchAll(typeof(Patch_GameLogout));
			val.PatchAll(typeof(Patch_PlayerOnDeath));
			Log.LogInfo((object)string.Format("[EilifMap] {0} v{1} loaded. Posting explored-map % to {2} every {3}s while on a server.", "Eilif Companion Client", "0.2.0", _url.Value, _intervalSeconds.Value));
			Log.LogInfo((object)("[EilifDeath] death-cause reporter armed (posts source:'eilif-death' to " + _url.Value + " when the local player dies on a server)."));
		}

		private void Update()
		{
			bool flag = IsOnServer();
			if (flag)
			{
				_postTimer += Time.unscaledDeltaTime;
				if (_postTimer >= (float)_intervalSeconds.Value)
				{
					_postTimer = 0f;
					ComputeAndPost("interval");
				}
			}
			else if (_wasConnected)
			{
				_postTimer = 0f;
				if (_lastPct >= 0f)
				{
					Post(_lastCachedPlayer, _lastCachedWorld, _lastPct, "disconnect(cached)");
				}
				_lastPct = -1f;
			}
			_wasConnected = flag;
		}

		internal static bool IsOnServer()
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || instance.IsServer())
			{
				return false;
			}
			if (instance.GetServerPeer() == null)
			{
				return false;
			}
			if ((Object)(object)Minimap.instance != (Object)null)
			{
				return (Object)(object)Player.m_localPlayer != (Object)null;
			}
			return false;
		}

		internal void ComputeAndPost(string reason)
		{
			try
			{
				Minimap instance = Minimap.instance;
				ZNet instance2 = ZNet.instance;
				Player localPlayer = Player.m_localPlayer;
				if (!((Object)(object)instance == (Object)null) && !((Object)(object)instance2 == (Object)null) && !((Object)(object)localPlayer == (Object)null))
				{
					float? num = ComputeExploredPct(instance);
					if (num.HasValue)
					{
						string playerName = localPlayer.GetPlayerName();
						string text = instance2.GetWorldName() ?? "";
						_lastCachedPlayer = playerName;
						_lastCachedWorld = text;
						_lastPct = num.Value;
						Post(playerName, text, num.Value, reason);
					}
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[EilifMap] compute/post failed: " + ex.Message));
			}
		}

		private static float? ComputeExploredPct(Minimap mm)
		{
			if (ExploredField == null || TextureSizeField == null)
			{
				return null;
			}
			if (!(ExploredField.GetValue(mm) is bool[] array) || array.Length == 0)
			{
				return null;
			}
			int num = (int)TextureSizeField.GetValue(mm);
			if (num <= 0 || array.Length < num * num)
			{
				return null;
			}
			float num2 = (float)num / 2f;
			float num3 = num2 * num2;
			long num4 = 0L;
			long num5 = 0L;
			for (int i = 0; i < num; i++)
			{
				float num6 = (float)i - num2;
				float num7 = num3 - num6 * num6;
				if (num7 < 0f)
				{
					continue;
				}
				float num8 = (float)Math.Sqrt(num7);
				int num9 = Math.Max(0, (int)Math.Ceiling(num2 - num8));
				int num10 = Math.Min(num - 1, (int)Math.Floor(num2 + num8));
				int num11 = i * num;
				for (int j = num9; j <= num10; j++)
				{
					num5++;
					if (array[num11 + j])
					{
						num4++;
					}
				}
			}
			if (num5 == 0L)
			{
				return null;
			}
			float val = (float)((double)num4 / (double)num5 * 100.0);
			return Math.Min(100f, val);
		}

		private void Post(string playerName, string world, float pct, string reason)
		{
			if (!string.IsNullOrEmpty(playerName) && Interlocked.CompareExchange(ref _postInFlight, 1, 0) == 0)
			{
				float num = (float)Math.Round(pct, 2);
				string json = "{\"schemaVersion\":1,\"game\":\"valheim\",\"source\":\"client-map\",\"playerName\":" + JsonStr(playerName) + ",\"world\":" + JsonStr(world) + ",\"exploredPct\":" + num.ToString("0.##", CultureInfo.InvariantCulture) + "}";
				string url = _url.Value;
				string token = _token.Value;
				string pctText = num.ToString("0.##", CultureInfo.InvariantCulture);
				Task.Run(() => PostAsync(url, token, json, playerName, world, pctText, reason));
			}
		}

		private async Task PostAsync(string url, string token, string json, string playerName, string world, string pctText, string reason)
		{
			try
			{
				HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, url);
				try
				{
					req.Content = (HttpContent)new StringContent(json, Encoding.UTF8, "application/json");
					if (!string.IsNullOrEmpty(token))
					{
						((HttpHeaders)req.Headers).TryAddWithoutValidation("Authorization", "Bearer " + token);
					}
					HttpResponseMessage val = await Http.SendAsync(req).ConfigureAwait(continueOnCapturedContext: false);
					try
					{
						if (val.IsSuccessStatusCode)
						{
							Log.LogInfo((object)("[EilifMap] posted " + pctText + "% for " + playerName + " (" + world + ") [" + reason + "]"));
						}
						else
						{
							Log.LogWarning((object)$"[EilifMap] post HTTP {(int)val.StatusCode} {val.ReasonPhrase} for {playerName}");
						}
					}
					finally
					{
						((IDisposable)val)?.Dispose();
					}
				}
				finally
				{
					((IDisposable)req)?.Dispose();
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("[EilifMap] post failed: " + ex.Message));
			}
			finally
			{
				Interlocked.Exchange(ref _postInFlight, 0);
			}
		}

		internal static void PostJson(string json, string tag, string successLine)
		{
			string url = IngestUrl;
			if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(json))
			{
				return;
			}
			string token = IngestToken;
			try
			{
				Task.Run(() => SendAsync(url, token, json, tag, successLine));
			}
			catch (Exception ex)
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogWarning((object)(tag + " could not queue post: " + ex.Message));
				}
			}
		}

		private static async Task SendAsync(string url, string token, string json, string tag, string successLine)
		{
			try
			{
				HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Post, url);
				try
				{
					req.Content = (HttpContent)new StringContent(json, Encoding.UTF8, "application/json");
					if (!string.IsNullOrEmpty(token))
					{
						((HttpHeaders)req.Headers).TryAddWithoutValidation("Authorization", "Bearer " + token);
					}
					HttpResponseMessage val = await Http.SendAsync(req).ConfigureAwait(continueOnCapturedContext: false);
					try
					{
						if (val.IsSuccessStatusCode)
						{
							ManualLogSource log = Log;
							if (log != null)
							{
								log.LogInfo((object)successLine);
							}
						}
						else
						{
							ManualLogSource log2 = Log;
							if (log2 != null)
							{
								log2.LogWarning((object)$"{tag} post HTTP {(int)val.StatusCode} {val.ReasonPhrase}");
							}
						}
					}
					finally
					{
						((IDisposable)val)?.Dispose();
					}
				}
				finally
				{
					((IDisposable)req)?.Dispose();
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log3 = Log;
				if (log3 != null)
				{
					log3.LogWarning((object)(tag + " post failed: " + ex.Message));
				}
			}
		}

		internal static string JsonStr(string s)
		{
			StringBuilder stringBuilder = new StringBuilder(s.Length + 2);
			stringBuilder.Append('"');
			foreach (char c in s)
			{
				switch (c)
				{
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '\b':
					stringBuilder.Append("\\b");
					continue;
				case '\f':
					stringBuilder.Append("\\f");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\r':
					stringBuilder.Append("\\r");
					continue;
				case '\t':
					stringBuilder.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			stringBuilder.Append('"');
			return stringBuilder.ToString();
		}
	}
	[HarmonyPatch(typeof(Game), "Logout")]
	internal static class Patch_GameLogout
	{
		private static void Prefix()
		{
			try
			{
				EilifMapTrackerPlugin.Instance?.ComputeAndPost("logout");
			}
			catch (Exception ex)
			{
				ManualLogSource log = EilifMapTrackerPlugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("[EilifMap] logout hook failed: " + ex.Message));
				}
			}
		}
	}
}