Decompiled source of PurpleTurtle Companion v1.3.11

BepInEx/plugins/PurpleTurtleCompanion/MagiCorp.PurpleTurtleCompanion.Client.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
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("MagiCorp.PurpleTurtleCompanion.Client")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+94a0159cc17e2ecee7b16678996a689ed3702a2d")]
[assembly: AssemblyProduct("MagiCorp.PurpleTurtleCompanion.Client")]
[assembly: AssemblyTitle("MagiCorp.PurpleTurtleCompanion.Client")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MagiCorp.PurpleTurtleCompanion;

[BepInPlugin("magicorp.purpleturtle.companion.client", "Purple Turtle Companion", "1.3.11")]
public sealed class ClientCompanion : BaseUnityPlugin
{
	private sealed class DamageState
	{
		public float Before;

		public float MaxHealth;

		public string Attacker = "";

		public string Victim = "";

		public string AttackerPrefab = "";

		public string VictimPrefab = "";

		public string AttackerId = "";

		public string VictimId = "";

		public bool AttackerPlayer;

		public bool VictimPlayer;

		public bool AttackerBoss;

		public bool VictimBoss;
	}

	private const string PluginVersion = "1.3.11";

	private const string EventRpc = "MagiCorp_QolAudit_v1";

	private const string HelloRpc = "MagiCorp_QolHello_v1";

	private const string AckRpc = "MagiCorp_QolAck_v1";

	private const string StatsRpc = "MagiCorp_PlayerStats_v1";

	private const string CombatRpc = "MagiCorp_Combat_v1";

	private const string FogRpc = "MagiCorp_MapFog_v1";

	private const string NetProbeRpc = "MagiCorp_NetProbe_v1";

	private const string NetReplyRpc = "MagiCorp_NetReply_v1";

	private const string ServerChatRpc = "MagiCorp_ServerChat_v1";

	private const string DeathRpc = "MagiCorp_PlayerDeath_v1";

	private static ClientCompanion I;

	private Harmony harmony;

	private bool combatHooked;

	private bool serverNativeFog;

	private bool fogSyncedThisConnection;

	private ConfigEntry<bool> RepeatFogSync;

	private ConfigEntry<float> FogSyncIntervalSeconds;

	private float nextScan;

	private float nextHello;

	private float nextStats;

	private float nextStatsForce;

	private float nextFogForce;

	private float nextAdminScan;

	private float lastDeathSent;

	private string lastStatsPayload = "";

	private string lastFogPayload = "";

	private string adminToolVersion = "absent";

	private string fogState = "pending";

	private int adminHooks;

	private int adminHookFailures;

	private int transportSamples;

	private int transportJitterSamples;

	private int transportPing;

	private double transportPingSum;

	private double transportPingMin = double.MaxValue;

	private double transportPingMax;

	private double transportJitterSum;

	private double transportPrevPing;

	private float transportLq = -1f;

	private float transportRq = -1f;

	private float transportOutBps;

	private float transportInBps;

	private ZRpc serverRpc;

	private bool ack;

	private readonly Queue<string> pending = new Queue<string>();

	private readonly HashSet<MethodBase> patchedAdminMethods = new HashSet<MethodBase>();

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

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

	private static readonly FieldInfo FogPixelSizeField = AccessTools.Field(typeof(Minimap), "m_pixelSize");

	private string lastAuditKey = "";

	private float lastAuditAt;

	private static readonly MethodInfo InstantLootSpawnMethod = typeof(Ragdoll).GetMethod("SpawnLoot", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[1] { typeof(Vector3) }, null);

	private void Awake()
	{
		//IL_0052: Unknown result type (might be due to invalid IL or missing references)
		//IL_005c: Expected O, but got Unknown
		I = this;
		RepeatFogSync = ((BaseUnityPlugin)this).Config.Bind<bool>("Fog", "RepeatSync", false, "Repeat client-side fog scans after the first successful sync. Purple Turtle uses server-native fog, so this should normally stay disabled.");
		FogSyncIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Fog", "RepeatIntervalSeconds", 30f, "Seconds between legacy client fog scans when RepeatSync is enabled (5-600). Ignored when the server provides native shared fog.");
		harmony = new Harmony("magicorp.purpleturtle.companion.client");
		TryHookCombat();
		TryHookPlayerDeath();
		TryHookInstantLoot();
		TryHookAdminTool(force: true);
		((MonoBehaviour)this).InvokeRepeating("FogTick", 5f, Mathf.Clamp(FogSyncIntervalSeconds.Value, 5f, 600f));
		((MonoBehaviour)this).InvokeRepeating("NetTick", 1f, 1f);
		((BaseUnityPlugin)this).Logger.LogInfo((object)"Purple Turtle Companion 1.3.11 loaded. Live stats, combat telemetry and AdminTool auditing enabled for Purple Turtle.");
	}

	private void OnDestroy()
	{
		((MonoBehaviour)this).CancelInvoke("FogTick");
		((MonoBehaviour)this).CancelInvoke("NetTick");
		Harmony obj = harmony;
		if (obj != null)
		{
			obj.UnpatchSelf();
		}
		I = null;
	}

	private void Update()
	{
		if (Time.unscaledTime < nextScan)
		{
			return;
		}
		nextScan = Time.unscaledTime + 0.25f;
		try
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || instance.IsServer())
			{
				ResetConnection();
				return;
			}
			TryHookAdminTool();
			TryHookCombat();
			ZNetPeer serverPeer = instance.GetServerPeer();
			if (serverPeer == null || !serverPeer.IsReady())
			{
				ResetConnection();
				return;
			}
			if (serverRpc != serverPeer.m_rpc)
			{
				ResetConnection();
				serverRpc = serverPeer.m_rpc;
				serverRpc.Register<string>("MagiCorp_QolAck_v1", (Action<ZRpc, string>)delegate(ZRpc rpc, string v)
				{
					if (rpc == serverRpc)
					{
						ack = true;
						serverNativeFog = !string.IsNullOrEmpty(v) && v.IndexOf("fog=server-native", StringComparison.OrdinalIgnoreCase) >= 0;
						if (serverNativeFog)
						{
							fogState = "server-native";
						}
					}
				});
				serverRpc.Register<string>("MagiCorp_NetProbe_v1", (Action<ZRpc, string>)OnNetProbe);
				serverRpc.Register<string, string>("MagiCorp_ServerChat_v1", (Action<ZRpc, string, string>)OnServerChat);
			}
			if (Time.unscaledTime >= nextHello)
			{
				nextHello = Time.unscaledTime + 10f;
				string text = ((adminToolVersion == "absent") ? "AdminTool=absent" : $"AdminTool={adminToolVersion};hooks={adminHooks};failures={adminHookFailures}");
				serverRpc.Invoke("MagiCorp_QolHello_v1", new object[1] { "1.3.11;Purple Turtle Companion;coverage=active;netprobe=1;" + text + ";" + ClientNetworkInfo() + ";fog=" + fogState });
			}
			if ((Object)(object)Player.m_localPlayer != (Object)null && Time.unscaledTime >= nextStats)
			{
				nextStats = Time.unscaledTime + 15f;
				SendStats();
			}
			if (!ack)
			{
				return;
			}
			for (int num = 0; num < 24; num++)
			{
				if (pending.Count <= 0)
				{
					break;
				}
				serverRpc.Invoke("MagiCorp_QolAudit_v1", new object[1] { pending.Dequeue() });
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogDebug((object)ex.Message);
		}
	}

	private void ResetConnection()
	{
		serverRpc = null;
		ack = false;
		serverNativeFog = false;
		fogSyncedThisConnection = false;
		nextHello = 0f;
		nextStats = 0f;
		nextStatsForce = 0f;
		nextFogForce = 0f;
		lastStatsPayload = "";
		lastFogPayload = "";
		pending.Clear();
	}

	private void OnNetProbe(ZRpc rpc, string sequence)
	{
		if (serverRpc == null || rpc != serverRpc || string.IsNullOrEmpty(sequence))
		{
			return;
		}
		try
		{
			serverRpc.Invoke("MagiCorp_NetReply_v1", new object[1] { sequence });
		}
		catch
		{
		}
	}

	private void OnServerChat(ZRpc rpc, string title, string text)
	{
		if (serverRpc == null || rpc != serverRpc || string.IsNullOrWhiteSpace(text))
		{
			return;
		}
		try
		{
			if ((Object)(object)Chat.instance != (Object)null)
			{
				((Terminal)Chat.instance).AddString(string.IsNullOrWhiteSpace(title) ? "Purple Turtle" : title, text, (Type)1, false);
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogDebug((object)("Server chat display failed: " + ex.Message));
		}
	}

	private void TryHookPlayerDeath()
	{
		//IL_0038: Unknown result type (might be due to invalid IL or missing references)
		//IL_0045: Expected O, but got Unknown
		try
		{
			MethodInfo methodInfo = AccessTools.Method(typeof(Player), "OnDeath", (Type[])null, (Type[])null);
			if (methodInfo != null)
			{
				harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(ClientCompanion), "PlayerDeathPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogDebug((object)("Player death hook failed: " + ex.Message));
		}
	}

	private static void PlayerDeathPostfix(Player __instance)
	{
		try
		{
			I?.ReportDeath(__instance);
		}
		catch
		{
		}
	}

	private void ReportDeath(Player p)
	{
		if ((Object)(object)p == (Object)null || (Object)(object)p != (Object)(object)Player.m_localPlayer || serverRpc == null || !ack || Time.unscaledTime - lastDeathSent < 3f)
		{
			return;
		}
		lastDeathSent = Time.unscaledTime;
		try
		{
			serverRpc.Invoke("MagiCorp_PlayerDeath_v1", new object[1] { Clean(p.GetPlayerName(), 80) });
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogDebug((object)("Death report failed: " + ex.Message));
		}
	}

	private void TryHookInstantLoot()
	{
		//IL_0057: Unknown result type (might be due to invalid IL or missing references)
		//IL_0064: Expected O, but got Unknown
		try
		{
			MethodInfo methodInfo = AccessTools.Method(typeof(Ragdoll), "Setup", (Type[])null, (Type[])null);
			if (methodInfo == null || InstantLootSpawnMethod == null)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Instant loot disabled: required vanilla Ragdoll methods not found.");
				return;
			}
			harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(ClientCompanion), "InstantLootPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Instant loot enabled for Purple Turtle (safe reflection path).");
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Instant loot hook failed: " + ex));
		}
	}

	private static void InstantLootPostfix(Ragdoll __instance)
	{
		//IL_0088: Unknown result type (might be due to invalid IL or missing references)
		//IL_0076: Unknown result type (might be due to invalid IL or missing references)
		//IL_008d: Unknown result type (might be due to invalid IL or missing references)
		//IL_009c: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)I == (Object)null || !I.ack || (Object)(object)__instance == (Object)null)
		{
			return;
		}
		try
		{
			ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
			if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner())
			{
				return;
			}
			ZDO zDO = component.GetZDO();
			if (zDO == null || zDO.GetInt(ZDOVars.s_drops, 0) <= 0)
			{
				return;
			}
			Vector3 val = (((Object)(object)__instance.m_lootSpawnJoint != (Object)null) ? __instance.m_lootSpawnJoint.transform.position : __instance.GetAverageBodyPosition());
			try
			{
				InstantLootSpawnMethod.Invoke(__instance, new object[1] { val });
				zDO.Set(ZDOVars.s_drops, 0, false);
			}
			catch (TargetInvocationException ex)
			{
				Exception? obj = ex.InnerException ?? ex;
				((BaseUnityPlugin)I).Logger.LogError((object)("Instant loot vanilla SpawnLoot failed; corpse loot retained for normal fade drop: " + obj));
			}
		}
		catch (Exception ex2)
		{
			((BaseUnityPlugin)I).Logger.LogError((object)("Instant loot failed safely; vanilla death flow continues: " + ex2));
		}
	}

	private void Report(string feature, string detail)
	{
		if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || (Object)(object)Player.m_localPlayer == (Object)null)
		{
			return;
		}
		feature = Clean(feature, 100);
		detail = Clean(detail, 800);
		string text = feature + "\t" + detail;
		if (!(text == lastAuditKey) || !(Time.unscaledTime - lastAuditAt < 0.35f))
		{
			lastAuditKey = text;
			lastAuditAt = Time.unscaledTime;
			if (pending.Count >= 1024)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Purple Turtle Companion audit queue full; report dropped");
				return;
			}
			pending.Enqueue(feature + "\t" + detail);
			((BaseUnityPlugin)this).Logger.LogWarning((object)("[PurpleTurtleAudit] " + feature + ": " + Clean(detail, 320)));
		}
	}

	private void FogTick()
	{
		if (serverNativeFog)
		{
			fogState = "server-native";
		}
		else if (fogSyncedThisConnection && !RepeatFogSync.Value)
		{
			fogState = "one-shot-complete";
		}
		else
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				return;
			}
			fogState = "attempt";
			try
			{
				SendFog();
				if (fogState.StartsWith("sent-", StringComparison.Ordinal) || fogState.StartsWith("ok-", StringComparison.Ordinal))
				{
					fogSyncedThisConnection = true;
				}
			}
			catch (Exception ex)
			{
				fogState = "timer-error-" + Clean(ex.GetType().Name, 32);
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Fog timer failed: " + ex.Message));
			}
		}
	}

	private void SendFog()
	{
		if (!ack || serverRpc == null || (Object)(object)Player.m_localPlayer == (Object)null)
		{
			fogState = "waiting";
			return;
		}
		if ((Object)(object)Minimap.instance == (Object)null)
		{
			fogState = "no-minimap";
			return;
		}
		try
		{
			Minimap instance = Minimap.instance;
			BitArray bitArray = FogOwnField?.GetValue(instance) as BitArray;
			int num = 0;
			float num2 = 0f;
			try
			{
				if (FogTextureSizeField?.GetValue(instance) is int num3)
				{
					num = num3;
				}
			}
			catch
			{
			}
			try
			{
				if (FogPixelSizeField?.GetValue(instance) is float num4)
				{
					num2 = num4;
				}
			}
			catch
			{
			}
			if (bitArray == null || bitArray.Length <= 0)
			{
				fogState = "no-explored";
				return;
			}
			int length = bitArray.Length;
			int num5 = (int)Math.Round(Math.Sqrt(length));
			if (num5 < 64 || num5 > 4096 || num5 * num5 != length)
			{
				fogState = "bad-mask-" + length.ToString(CultureInfo.InvariantCulture);
				return;
			}
			if (num2 <= 0f || float.IsNaN(num2) || float.IsInfinity(num2))
			{
				fogState = "bad-pixel-size";
				return;
			}
			byte[] array = new byte[13613];
			int num6 = 0;
			float num7 = (float)num5 / 2f;
			float num8 = 165f;
			for (int i = 0; i < 330; i++)
			{
				int num9 = Mathf.RoundToInt(((float)i - num8) * 64f / num2 + num7);
				if (num9 < 0 || num9 >= num5)
				{
					continue;
				}
				for (int j = 0; j < 330; j++)
				{
					int num10 = Mathf.RoundToInt(((float)j - num8) * 64f / num2 + num7);
					if (num10 >= 0 && num10 < num5)
					{
						int index = num9 * num5 + num10;
						if (bitArray[index])
						{
							int num11 = i * 330 + j;
							array[num11 >> 3] |= (byte)(1 << (num11 & 7));
							num6++;
						}
					}
				}
			}
			string text = 330.ToString(CultureInfo.InvariantCulture) + "\t" + Convert.ToBase64String(array);
			bool flag = Time.unscaledTime >= nextFogForce;
			if (text != lastFogPayload || flag)
			{
				serverRpc.Invoke("MagiCorp_MapFog_v1", new object[1] { text });
				lastFogPayload = text;
				nextFogForce = Time.unscaledTime + 300f;
				fogState = "sent-" + num6.ToString(CultureInfo.InvariantCulture) + "of" + 108900.ToString(CultureInfo.InvariantCulture);
				((BaseUnityPlugin)this).Logger.LogInfo((object)$"Fog sync: {num6}/{108900} website cells from own map (source {num5}x{num5}, minimap pixel {num2:0.###}m, texture {num})");
			}
			else
			{
				fogState = "ok-" + num6.ToString(CultureInfo.InvariantCulture) + "of" + 108900.ToString(CultureInfo.InvariantCulture);
			}
		}
		catch (Exception ex)
		{
			fogState = "error-" + Clean(ex.GetType().Name, 40);
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Fog telemetry failed: " + ex.Message));
		}
	}

	private void NetTick()
	{
		try
		{
			ZNet instance = ZNet.instance;
			ZNetPeer val = ((instance != null) ? instance.GetServerPeer() : null);
			if (val?.m_socket != null)
			{
				float num = default(float);
				float num2 = default(float);
				int val2 = default(int);
				float val3 = default(float);
				float val4 = default(float);
				val.m_socket.GetConnectionQuality(ref num, ref num2, ref val2, ref val3, ref val4);
				double num3 = Math.Max(0, val2);
				transportPing = val2;
				transportLq = num;
				transportRq = num2;
				transportOutBps = Math.Max(0f, val3);
				transportInBps = Math.Max(0f, val4);
				transportSamples++;
				transportPingSum += num3;
				transportPingMin = Math.Min(transportPingMin, num3);
				transportPingMax = Math.Max(transportPingMax, num3);
				if (transportSamples > 1)
				{
					transportJitterSum += Math.Abs(num3 - transportPrevPing);
					transportJitterSamples++;
				}
				transportPrevPing = num3;
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogDebug((object)("Transport sample failed: " + ex.Message));
		}
	}

	private string ClientNetworkInfo()
	{
		try
		{
			if (transportSamples <= 0)
			{
				return "transport=unavailable";
			}
			double num = transportPingSum / (double)transportSamples;
			double num2 = ((transportPingMin == double.MaxValue) ? 0.0 : transportPingMin);
			double num3 = ((transportJitterSamples > 0) ? (transportJitterSum / (double)transportJitterSamples) : 0.0);
			return "transport=" + transportPing.ToString(CultureInfo.InvariantCulture) + "," + transportLq.ToString("0.###", CultureInfo.InvariantCulture) + "," + transportRq.ToString("0.###", CultureInfo.InvariantCulture) + "," + transportOutBps.ToString("0", CultureInfo.InvariantCulture) + "," + transportInBps.ToString("0", CultureInfo.InvariantCulture) + "," + num.ToString("0.0", CultureInfo.InvariantCulture) + "," + num2.ToString("0.0", CultureInfo.InvariantCulture) + "," + transportPingMax.ToString("0.0", CultureInfo.InvariantCulture) + "," + num3.ToString("0.0", CultureInfo.InvariantCulture) + "," + transportSamples.ToString(CultureInfo.InvariantCulture);
		}
		catch (Exception ex)
		{
			return "transport=error-" + Clean(ex.GetType().Name, 32);
		}
	}

	private void SendStats()
	{
		if (!ack || serverRpc == null || (Object)(object)Game.instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null)
		{
			return;
		}
		try
		{
			string text = BuildStatsJson();
			if (!string.IsNullOrEmpty(text))
			{
				bool flag = Time.unscaledTime >= nextStatsForce;
				if (text != lastStatsPayload || flag)
				{
					serverRpc.Invoke("MagiCorp_PlayerStats_v1", new object[1] { text });
					lastStatsPayload = text;
					nextStatsForce = Time.unscaledTime + 300f;
				}
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogDebug((object)("Stats telemetry failed: " + ex.Message));
		}
	}

	private string BuildStatsJson()
	{
		PlayerProfile playerProfile = Game.instance.GetPlayerProfile();
		if (playerProfile == null)
		{
			return null;
		}
		Array array = (Array)(typeof(PlayerProfile).GetField("m_playerStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(playerProfile));
		if (array == null || array.Length == 0)
		{
			return null;
		}
		object value = array.GetValue(0);
		if (value == null)
		{
			return null;
		}
		Type type = value.GetType();
		StringBuilder stringBuilder = new StringBuilder(8192);
		stringBuilder.Append('{').Append("\"schema\":2,\"character\":\"").Append(JsonEscape(playerProfile.GetName()))
			.Append("\",\"player_id\":")
			.Append(playerProfile.GetPlayerID());
		AppendMap(stringBuilder, "stats", type.GetField("m_stats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value) as IDictionary);
		object? obj = type.GetField("m_enemyStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value);
		IDictionary dict = null;
		if (obj is Array { Length: >0 } array2)
		{
			dict = array2.GetValue(0) as IDictionary;
		}
		AppendMap(stringBuilder, "enemies", dict);
		AppendMap(stringBuilder, "food", type.GetField("m_foodEatenStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value) as IDictionary);
		AppendMap(stringBuilder, "pieces", type.GetField("m_piecesPlacedStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value) as IDictionary);
		AppendMap(stringBuilder, "crafted", type.GetField("m_itemCraftStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value) as IDictionary);
		AppendMap(stringBuilder, "picked_up", type.GetField("m_itemPickupStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value) as IDictionary);
		AppendMap(stringBuilder, "harvested", type.GetField("m_pickableStats", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value) as IDictionary);
		stringBuilder.Append('}');
		return stringBuilder.ToString();
	}

	private static void AppendMap(StringBuilder sb, string name, IDictionary dict)
	{
		sb.Append(",\"").Append(name).Append("\":{");
		if (dict != null)
		{
			List<KeyValuePair<string, float>> list = new List<KeyValuePair<string, float>>();
			foreach (DictionaryEntry item in dict)
			{
				float value;
				try
				{
					value = Convert.ToSingle(item.Value, CultureInfo.InvariantCulture);
				}
				catch
				{
					continue;
				}
				if (!(Math.Abs(value) < 0.0001f))
				{
					list.Add(new KeyValuePair<string, float>(Convert.ToString(item.Key, CultureInfo.InvariantCulture), value));
				}
			}
			list.Sort((KeyValuePair<string, float> a, KeyValuePair<string, float> b) => string.CompareOrdinal(a.Key, b.Key));
			for (int num = 0; num < list.Count; num++)
			{
				if (num > 0)
				{
					sb.Append(',');
				}
				sb.Append('"').Append(JsonEscape(list[num].Key)).Append("\":")
					.Append(list[num].Value.ToString("0.###", CultureInfo.InvariantCulture));
			}
		}
		sb.Append('}');
	}

	private static string JsonEscape(string s)
	{
		return (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ")
			.Replace("\n", " ");
	}

	private void TryHookCombat()
	{
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_0065: Expected O, but got Unknown
		//IL_0065: Expected O, but got Unknown
		if (combatHooked)
		{
			return;
		}
		try
		{
			MethodInfo methodInfo = AccessTools.Method(typeof(Character), "ApplyDamage", (Type[])null, (Type[])null);
			if (!(methodInfo == null))
			{
				harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(ClientCompanion), "DamagePrefix", (Type[])null), new HarmonyMethod(typeof(ClientCompanion), "DamagePostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				combatHooked = true;
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Combat telemetry hook failed: " + ex.Message));
		}
	}

	private static string Prefab(Character c)
	{
		try
		{
			return ((Object)(object)c == (Object)null) ? "" : Utils.GetPrefabName(((Component)c).gameObject);
		}
		catch
		{
			return ((c != null) ? ((Object)c).name : null) ?? "";
		}
	}

	private static string CharId(Character c)
	{
		//IL_000a: Unknown result type (might be due to invalid IL or missing references)
		//IL_000f: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			return ((Object)(object)c == (Object)null) ? "" : ((object)c.GetZDOID()/*cast due to .constrained prefix*/).ToString();
		}
		catch
		{
			return "";
		}
	}

	private static void DamagePrefix(Character __instance, HitData hit, out DamageState __state)
	{
		__state = null;
		try
		{
			if ((Object)(object)I == (Object)null || (Object)(object)__instance == (Object)null || hit == null)
			{
				return;
			}
			Character attacker = hit.GetAttacker();
			bool flag = (Object)(object)attacker != (Object)null && attacker.IsPlayer();
			bool flag2 = __instance.IsPlayer();
			bool flag3 = (Object)(object)attacker != (Object)null && attacker.IsBoss();
			bool flag4 = __instance.IsBoss();
			if (!flag && !flag2 && !flag3 && !flag4)
			{
				return;
			}
			object obj;
			if (flag)
			{
				Player val = (Player)(object)((attacker is Player) ? attacker : null);
				if (val != null)
				{
					obj = val.GetPlayerName();
					goto IL_009d;
				}
			}
			obj = (((Object)(object)attacker != (Object)null) ? attacker.GetHoverName() : "environment");
			goto IL_009d;
			IL_00bd:
			string text;
			string s = (string)text;
			string s2;
			__state = new DamageState
			{
				Before = __instance.GetHealth(),
				MaxHealth = __instance.GetMaxHealth(),
				Attacker = Clean(s2, 100),
				Victim = Clean(s, 100),
				AttackerPlayer = flag,
				VictimPlayer = flag2,
				AttackerBoss = flag3,
				VictimBoss = flag4,
				AttackerPrefab = Clean(Prefab(attacker), 100),
				VictimPrefab = Clean(Prefab(__instance), 100),
				AttackerId = Clean(CharId(attacker), 100),
				VictimId = Clean(CharId(__instance), 100)
			};
			return;
			IL_009d:
			s2 = (string)obj;
			if (flag2)
			{
				Player val2 = (Player)(object)((__instance is Player) ? __instance : null);
				if (val2 != null)
				{
					text = val2.GetPlayerName();
					goto IL_00bd;
				}
			}
			text = __instance.GetHoverName();
			goto IL_00bd;
		}
		catch
		{
		}
	}

	private static void DamagePostfix(Character __instance, DamageState __state)
	{
		try
		{
			if (!((Object)(object)I == (Object)null) && __state != null && !((Object)(object)__instance == (Object)null) && I.ack && I.serverRpc != null)
			{
				float health = __instance.GetHealth();
				float num = __state.Before - health;
				if (!(num <= 0.001f))
				{
					bool flag = __state.Before > 0.001f && health <= 0.001f;
					string text = string.Join("\t", "damage", __state.Attacker, __state.Victim, num.ToString("0.###", CultureInfo.InvariantCulture), __state.AttackerPlayer ? "1" : "0", __state.VictimPlayer ? "1" : "0", __state.AttackerPrefab, __state.VictimPrefab, __state.AttackerBoss ? "1" : "0", __state.VictimBoss ? "1" : "0", flag ? "1" : "0", __state.AttackerId, __state.VictimId, __state.MaxHealth.ToString("0.###", CultureInfo.InvariantCulture));
					I.serverRpc.Invoke("MagiCorp_Combat_v1", new object[1] { text });
				}
			}
		}
		catch
		{
		}
	}

	private void TryHookAdminTool(bool force = false)
	{
		if (!force && Time.unscaledTime < nextAdminScan)
		{
			return;
		}
		nextAdminScan = Time.unscaledTime + 5f;
		try
		{
			Type type = AccessTools.TypeByName("ValheimAdminTool.AdminActions");
			if (type == null)
			{
				adminToolVersion = "absent";
				adminHooks = 0;
				adminHookFailures = 0;
				return;
			}
			Assembly assembly = type.Assembly;
			adminToolVersion = GetAdminToolVersion(assembly);
			int added = 0;
			int failed = 0;
			MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public);
			foreach (MethodInfo m in methods)
			{
				if (ShouldHookAdminAction(m))
				{
					PatchAudit(m, ref added, ref failed);
				}
			}
			PatchNamed(assembly, "ValheimAdminTool.AdminEsp", "SetCategoryEnabled", ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminSpeeds", "SetPercent", ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminFun", "SetPercent", ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminPickup", "SetRadius", ref added, ref failed);
			PatchMapTeleport(assembly, ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminVirtualWorkbenches", "Open", ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminToolRpcs", "RequestTerrainReset", ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminToolRpcs", "RequestWorldReset", ref added, ref failed);
			PatchNamed(assembly, "ValheimAdminTool.AdminToolRpcs", "RequestTool", ref added, ref failed);
			adminHooks = patchedAdminMethods.Count;
			adminHookFailures += failed;
			if (added > 0 || force)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)($"Valheim Admin Tool {adminToolVersion} audit coverage: {adminHooks} methods active" + ((adminHookFailures > 0) ? $", {adminHookFailures} failed" : "")));
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("AdminTool audit scan failed: " + ex.Message));
		}
	}

	private static bool ShouldHookAdminAction(MethodInfo m)
	{
		string name = m.Name;
		if (name.StartsWith("On", StringComparison.Ordinal) && (name.EndsWith("Toggled", StringComparison.Ordinal) || name.EndsWith("Clicked", StringComparison.Ordinal) || name.EndsWith("Changed", StringComparison.Ordinal)))
		{
			return true;
		}
		if (name.StartsWith("Spawn", StringComparison.Ordinal))
		{
			return true;
		}
		if (name == "SetSkill")
		{
			return true;
		}
		return false;
	}

	private string GetAdminToolVersion(Assembly asm)
	{
		try
		{
			FieldInfo fieldInfo = asm.GetType("ValheimAdminTool.PluginInfo")?.GetField("Version", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			if (fieldInfo != null)
			{
				return Convert.ToString(fieldInfo.GetRawConstantValue(), CultureInfo.InvariantCulture) ?? "unknown";
			}
		}
		catch
		{
		}
		return asm.GetName().Version?.ToString() ?? "unknown";
	}

	private void PatchNamed(Assembly asm, string typeName, string methodName, ref int added, ref int failed)
	{
		Type type = asm.GetType(typeName);
		if (type == null)
		{
			return;
		}
		foreach (MethodInfo item in from x in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
			where x.Name == methodName
			select x)
		{
			PatchAudit(item, ref added, ref failed);
		}
	}

	private void PatchMapTeleport(Assembly asm, ref int added, ref int failed)
	{
		//IL_007a: Unknown result type (might be due to invalid IL or missing references)
		//IL_008f: Unknown result type (might be due to invalid IL or missing references)
		//IL_009c: Expected O, but got Unknown
		//IL_009c: Expected O, but got Unknown
		Type type = asm.GetType("ValheimAdminTool.AdminMapTeleport");
		if (type == null)
		{
			return;
		}
		foreach (MethodInfo item in from x in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
			where x.Name == "HandleClick"
			select x)
		{
			if (!patchedAdminMethods.Contains(item))
			{
				try
				{
					harmony.Patch((MethodBase)item, new HarmonyMethod(typeof(ClientCompanion), "MapTeleportPrefix", (Type[])null), new HarmonyMethod(typeof(ClientCompanion), "MapTeleportPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
					patchedAdminMethods.Add(item);
					added++;
				}
				catch (Exception ex)
				{
					failed++;
					((BaseUnityPlugin)this).Logger.LogWarning((object)("AdminTool map teleport hook failed: " + ex.Message));
				}
			}
		}
	}

	private static void MapTeleportPrefix(ref Vector3 __state)
	{
		//IL_004b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Unknown result type (might be due to invalid IL or missing references)
		//IL_002e: Unknown result type (might be due to invalid IL or missing references)
		//IL_001d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			__state = (Vector3)(((Object)(object)Player.m_localPlayer != (Object)null) ? ((Component)Player.m_localPlayer).transform.position : new Vector3(float.NaN, float.NaN, float.NaN));
		}
		catch
		{
			__state = new Vector3(float.NaN, float.NaN, float.NaN);
		}
	}

	private static void MapTeleportPostfix(Vector3 __state)
	{
		//IL_001a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		//IL_003c: Unknown result type (might be due to invalid IL or missing references)
		//IL_003d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0076: Unknown result type (might be due to invalid IL or missing references)
		//IL_0084: Unknown result type (might be due to invalid IL or missing references)
		//IL_0092: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			if (!((Object)(object)I == (Object)null) && !((Object)(object)Player.m_localPlayer == (Object)null) && !float.IsNaN(__state.x))
			{
				Vector3 position = ((Component)Player.m_localPlayer).transform.position;
				float num = Vector3.Distance(__state, position);
				if (!(num < 20f))
				{
					I.Report("AdminTool.MapTeleport", string.Format(CultureInfo.InvariantCulture, "teleported {0:0.0}m: ({1:0.0}, {2:0.0}, {3:0.0}) -> ({4:0.0}, {5:0.0}, {6:0.0})", num, __state.x, __state.y, __state.z, position.x, position.y, position.z));
				}
			}
		}
		catch (Exception ex)
		{
			ClientCompanion i = I;
			if (i != null)
			{
				((BaseUnityPlugin)i).Logger.LogDebug((object)("AdminTool map teleport audit failed: " + ex.Message));
			}
		}
	}

	private void PatchAudit(MethodBase m, ref int added, ref int failed)
	{
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		//IL_003e: Expected O, but got Unknown
		if (m == null || patchedAdminMethods.Contains(m))
		{
			return;
		}
		try
		{
			harmony.Patch(m, new HarmonyMethod(typeof(ClientCompanion), "AdminAuditPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			patchedAdminMethods.Add(m);
			added++;
		}
		catch (Exception ex)
		{
			failed++;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("AdminTool hook failed: " + m.DeclaringType?.Name + "." + m.Name + ": " + ex.Message));
		}
	}

	private static void AdminAuditPrefix(MethodBase __originalMethod, object[] __args)
	{
		if ((Object)(object)I == (Object)null || __originalMethod == null)
		{
			return;
		}
		try
		{
			string owner = __originalMethod.DeclaringType?.Name ?? "AdminTool";
			string name = __originalMethod.Name;
			ParameterInfo[] parameters = __originalMethod.GetParameters();
			List<string> list = new List<string>();
			for (int i = 0; i < ((__args != null) ? __args.Length : 0); i++)
			{
				string text = ((i < parameters.Length) ? parameters[i].Name : ("arg" + i));
				list.Add(text + "=" + Describe(__args[i]));
			}
			string feature = HumanFeature(owner, name);
			I.Report(feature, (list.Count == 0) ? "attempt" : ("attempt: " + string.Join(" ", list)));
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)I).Logger.LogDebug((object)("AdminTool audit formatting failed: " + ex.Message));
		}
	}

	private static string HumanFeature(string owner, string method)
	{
		if (method.StartsWith("On") && method.EndsWith("Toggled"))
		{
			return "AdminTool." + method.Substring(2, method.Length - 9);
		}
		if (method.StartsWith("On") && method.EndsWith("Clicked"))
		{
			return "AdminTool." + method.Substring(2, method.Length - 9);
		}
		if (method.StartsWith("On") && method.EndsWith("Changed"))
		{
			return "AdminTool." + method.Substring(2, method.Length - 9);
		}
		switch (owner)
		{
		case "AdminEsp":
			return "AdminTool.ESP";
		case "AdminSpeeds":
			return "AdminTool.Speed";
		case "AdminFun":
			return "AdminTool.Fun";
		case "AdminPickup":
			return "AdminTool.PickupRadius";
		case "AdminMapTeleport":
			return "AdminTool.MapTeleport";
		case "AdminVirtualWorkbenches":
			return "AdminTool.VirtualWorkbench";
		default:
			_ = owner == "AdminToolRpcs";
			return "AdminTool." + method;
		}
	}

	private static string Describe(object value)
	{
		if (value == null)
		{
			return "null";
		}
		Type type = value.GetType();
		if (value is string || type.IsPrimitive || type.IsEnum || value is decimal)
		{
			return Clean(Convert.ToString(value, CultureInfo.InvariantCulture), 140);
		}
		string[] array = new string[4] { "PrefabName", "DisplayName", "Name", "Id" };
		foreach (string name in array)
		{
			try
			{
				PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (property != null)
				{
					object value2 = property.GetValue(value, null);
					if (value2 != null)
					{
						return Clean(Convert.ToString(value2, CultureInfo.InvariantCulture), 140);
					}
				}
				FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field != null)
				{
					object value3 = field.GetValue(value);
					if (value3 != null)
					{
						return Clean(Convert.ToString(value3, CultureInfo.InvariantCulture), 140);
					}
				}
			}
			catch
			{
			}
		}
		return Clean(Convert.ToString(value, CultureInfo.InvariantCulture) ?? type.Name, 140);
	}

	private static string Clean(string s, int max)
	{
		s = new string((s ?? "").Select((char c) => (!char.IsControl(c)) ? c : ' ').ToArray());
		if (s.Length <= max)
		{
			return s;
		}
		return s.Substring(0, max);
	}
}