Decompiled source of TickProfiler v0.2.5

MagiCorp.ValheimTickProfiler.dll

Decompiled 8 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
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.ValheimTickProfiler")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+8a7348b5dbdb8712868f3841af346c25901bdbff")]
[assembly: AssemblyProduct("MagiCorp.ValheimTickProfiler")]
[assembly: AssemblyTitle("MagiCorp.ValheimTickProfiler")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MagiCorp.ValheimTickProfiler;

[BepInPlugin("magicorp.valheim.tickprofiler", "MagiCorp Valheim Tick Profiler", "0.2.5")]
public sealed class TickProfilerPlugin : BaseUnityPlugin
{
	private readonly struct TargetSpec
	{
		public readonly string TypeName;

		public readonly string MethodName;

		public readonly string Label;

		public readonly bool Deep;

		public TargetSpec(string typeName, string methodName, string label, bool deep)
		{
			TypeName = typeName;
			MethodName = methodName;
			Label = label;
			Deep = deep;
		}
	}

	private struct ScopeFrame
	{
		public Metric Metric;

		public long StartTicks;

		public long ChildTicks;
	}

	private sealed class Metric
	{
		public readonly string Name;

		public readonly string Kind;

		private long _calls;

		private long _selfTicks;

		private long _inclusiveTicks;

		private long _maxInclusiveTicks;

		public Metric(string name, string kind)
		{
			Name = name;
			Kind = kind;
		}

		public void Add(long selfTicks, long inclusiveTicks)
		{
			Interlocked.Increment(ref _calls);
			Interlocked.Add(ref _selfTicks, selfTicks);
			Interlocked.Add(ref _inclusiveTicks, inclusiveTicks);
			long num = Volatile.Read(in _maxInclusiveTicks);
			while (inclusiveTicks > num)
			{
				long num2 = Interlocked.CompareExchange(ref _maxInclusiveTicks, inclusiveTicks, num);
				if (num2 != num)
				{
					num = num2;
					continue;
				}
				break;
			}
		}

		public MetricRaw SnapshotAndReset()
		{
			return new MetricRaw(Interlocked.Exchange(ref _calls, 0L), Interlocked.Exchange(ref _selfTicks, 0L), Interlocked.Exchange(ref _inclusiveTicks, 0L), Interlocked.Exchange(ref _maxInclusiveTicks, 0L));
		}
	}

	private readonly struct MetricRaw
	{
		public readonly long Calls;

		public readonly long SelfTicks;

		public readonly long InclusiveTicks;

		public readonly long MaxInclusiveTicks;

		public MetricRaw(long calls, long selfTicks, long inclusiveTicks, long maxInclusiveTicks)
		{
			Calls = calls;
			SelfTicks = selfTicks;
			InclusiveTicks = inclusiveTicks;
			MaxInclusiveTicks = maxInclusiveTicks;
		}
	}

	private sealed class MetricSnapshot
	{
		public string Name;

		public string Kind;

		public double SelfMsPerSec;

		public double InclusiveMsPerSec;

		public double CpuPct;

		public double CallsPerSec;

		public double AvgSelfUs;

		public double MaxInclusiveMs;

		public static MetricSnapshot From(Metric metric, MetricRaw raw, double seconds)
		{
			double num = ToMilliseconds(raw.SelfTicks);
			return new MetricSnapshot
			{
				Name = metric.Name,
				Kind = metric.Kind,
				SelfMsPerSec = num / seconds,
				InclusiveMsPerSec = ToMilliseconds(raw.InclusiveTicks) / seconds,
				CpuPct = num / (seconds * 1000.0) * 100.0,
				CallsPerSec = (double)raw.Calls / seconds,
				AvgSelfUs = ((raw.Calls > 0) ? (num * 1000.0 / (double)raw.Calls) : 0.0),
				MaxInclusiveMs = ToMilliseconds(raw.MaxInclusiveTicks)
			};
		}
	}

	private sealed class GapStats
	{
		private readonly double[] _samples;

		private int _sampleCount;

		private int _next;

		private long _count;

		private double _sum;

		private double _max;

		private long _over20;

		private long _over40;

		private long _over100;

		public GapStats(int capacity)
		{
			_samples = new double[capacity];
		}

		public void Add(double ms)
		{
			_count++;
			_sum += ms;
			if (ms > _max)
			{
				_max = ms;
			}
			if (ms > 20.0)
			{
				_over20++;
			}
			if (ms > 40.0)
			{
				_over40++;
			}
			if (ms > 100.0)
			{
				_over100++;
			}
			_samples[_next++] = ms;
			if (_next == _samples.Length)
			{
				_next = 0;
			}
			if (_sampleCount < _samples.Length)
			{
				_sampleCount++;
			}
		}

		public GapSnapshot SnapshotAndReset()
		{
			double p95Ms = 0.0;
			if (_sampleCount > 0)
			{
				double[] array = new double[_sampleCount];
				Array.Copy(_samples, array, _sampleCount);
				Array.Sort(array);
				int val = Math.Min(array.Length - 1, (int)Math.Ceiling((double)array.Length * 0.95) - 1);
				p95Ms = array[Math.Max(0, val)];
			}
			GapSnapshot result = new GapSnapshot
			{
				Count = _count,
				AvgMs = ((_count > 0) ? (_sum / (double)_count) : 0.0),
				P95Ms = p95Ms,
				MaxMs = _max,
				Over20Ms = _over20,
				Over40Ms = _over40,
				Over100Ms = _over100
			};
			_sampleCount = 0;
			_next = 0;
			_count = 0L;
			_sum = 0.0;
			_max = 0.0;
			_over20 = 0L;
			_over40 = 0L;
			_over100 = 0L;
			return result;
		}
	}

	private struct GapSnapshot
	{
		public long Count;

		public double AvgMs;

		public double P95Ms;

		public double MaxMs;

		public long Over20Ms;

		public long Over40Ms;

		public long Over100Ms;
	}

	private sealed class ReportSnapshot
	{
		public DateTime TimestampUtc;

		public bool Requested;

		public double WallSeconds;

		public double UpdateHz;

		public double FixedHz;

		public double NominalFixedHz;

		public double FixedDeltaMs;

		public double TimeScale;

		public GapSnapshot UpdateGap;

		public GapSnapshot FixedGap;

		public double CpuOneCorePct;

		public double CpuVmPct;

		public double ManagedMiB;

		public int Gc0;

		public int Gc1;

		public int Gc2;

		public int PatchedMethods;

		public List<MetricSnapshot> Metrics;

		public string[] PatchFailures;
	}

	public const string PluginGuid = "magicorp.valheim.tickprofiler";

	public const string PluginName = "MagiCorp Valheim Tick Profiler";

	public const string PluginVersion = "0.2.5";

	private static readonly long Frequency = Stopwatch.Frequency;

	private static readonly Dictionary<MethodBase, Metric> MethodMetrics = new Dictionary<MethodBase, Metric>();

	private static readonly List<Metric> Metrics = new List<Metric>();

	private static Harmony _harmony;

	private static ManualLogSource _log;

	private static bool _profilingEnabled;

	private bool _serverMode;

	private bool _rpcRegistered;

	private readonly HashSet<long> _overlayPeers = new HashSet<long>();

	private string _latestOverlayText = string.Empty;

	private const string RpcHello = "MagiCorp_TickProfiler_Hello_v1";

	private const string RpcSnapshot = "MagiCorp_TickProfiler_Snapshot_v1";

	[ThreadStatic]
	private static ScopeFrame[] _scopeStack;

	[ThreadStatic]
	private static int _scopeDepth;

	private ConfigEntry<bool> _enabled;

	private ConfigEntry<float> _reportInterval;

	private ConfigEntry<int> _topConsumers;

	private ConfigEntry<bool> _deepGameProfiling;

	private ConfigEntry<bool> _profileModPatches;

	private ConfigEntry<bool> _logPeriodicSummary;

	private ConfigEntry<bool> _writeStatusJson;

	private ConfigEntry<float> _spikeThresholdMs;

	private readonly GapStats _updateGaps = new GapStats(1024);

	private readonly GapStats _fixedGaps = new GapStats(1024);

	private readonly List<string> _patchFailures = new List<string>();

	private Process _process;

	private TimeSpan _lastCpu;

	private long _lastReportTicks;

	private long _lastUpdateTicks;

	private long _lastFixedTicks;

	private long _updates;

	private long _fixedUpdates;

	private int _lastGc0;

	private int _lastGc1;

	private int _lastGc2;

	private string _statusPath;

	private string _textPath;

	private string _requestPath;

	private static readonly TargetSpec[] CoreTargets = new TargetSpec[38]
	{
		new TargetSpec("Game", "Update", "Game.Update", deep: false),
		new TargetSpec("Game", "FixedUpdate", "Game.FixedUpdate", deep: false),
		new TargetSpec("ZNet", "Update", "Network/ZNet.Update", deep: false),
		new TargetSpec("ZNet", "FixedUpdate", "Network/ZNet.FixedUpdate", deep: false),
		new TargetSpec("ZNet", "LateUpdate", "Network/ZNet.LateUpdate", deep: false),
		new TargetSpec("ZNet", "UpdatePeers", "Network/ZNet.UpdatePeers", deep: false),
		new TargetSpec("ZNet", "UpdateSave", "World/ZNet.UpdateSave", deep: false),
		new TargetSpec("ZDOMan", "Update", "Network/ZDOMan.Update", deep: false),
		new TargetSpec("ZDOMan", "SendZDOToPeers2", "Network/ZDOMan.SendZDOToPeers2", deep: false),
		new TargetSpec("ZDOMan", "SendZDOs", "Network/ZDOMan.SendZDOs", deep: false),
		new TargetSpec("ZDOMan", "CreateSyncList", "Network/ZDOMan.CreateSyncList", deep: false),
		new TargetSpec("ZDOMan", "RPC_ZDOData", "Network/ZDOMan.RPC_ZDOData", deep: false),
		new TargetSpec("ZNetScene", "Update", "World/ZNetScene.Update", deep: false),
		new TargetSpec("ZNetScene", "CreateDestroyObjects", "World/ZNetScene.CreateDestroyObjects", deep: false),
		new TargetSpec("ZoneSystem", "Update", "World/ZoneSystem.Update", deep: false),
		new TargetSpec("SpawnSystem", "UpdateSpawning", "World/SpawnSystem.UpdateSpawning", deep: false),
		new TargetSpec("EnvMan", "Update", "World/EnvMan.Update", deep: false),
		new TargetSpec("EnvMan", "FixedUpdate", "World/EnvMan.FixedUpdate", deep: false),
		new TargetSpec("Character", "CustomFixedUpdate", "Entities/Character.CustomFixedUpdate", deep: true),
		new TargetSpec("MonsterAI", "UpdateAI", "AI/MonsterAI.UpdateAI", deep: true),
		new TargetSpec("BaseAI", "UpdateAI", "AI/BaseAI.UpdateAI", deep: true),
		new TargetSpec("AnimalAI", "UpdateAI", "AI/AnimalAI.UpdateAI", deep: true),
		new TargetSpec("ZSyncTransform", "CustomFixedUpdate", "Network/ZSyncTransform.CustomFixedUpdate", deep: true),
		new TargetSpec("ZSyncTransform", "OwnerSync", "Network/ZSyncTransform.OwnerSync", deep: true),
		new TargetSpec("ZSyncTransform", "ClientSync", "Network/ZSyncTransform.ClientSync", deep: true),
		new TargetSpec("ZSyncAnimation", "CustomFixedUpdate", "Network/ZSyncAnimation.CustomFixedUpdate", deep: true),
		new TargetSpec("Projectile", "FixedUpdate", "Entities/Projectile.FixedUpdate", deep: true),
		new TargetSpec("Ship", "CustomFixedUpdate", "Entities/Ship.CustomFixedUpdate", deep: true),
		new TargetSpec("Fish", "CustomFixedUpdate", "Entities/Fish.CustomFixedUpdate", deep: true),
		new TargetSpec("ItemDrop", "SlowUpdate", "Items/ItemDrop.SlowUpdate", deep: true),
		new TargetSpec("ItemDrop", "PickupUpdate", "Items/ItemDrop.PickupUpdate", deep: true),
		new TargetSpec("ItemDrop", "EatUpdate", "Items/ItemDrop.EatUpdate", deep: true),
		new TargetSpec("WearNTear", "UpdateWear", "Build/WearNTear.UpdateWear", deep: true),
		new TargetSpec("WearNTear", "UpdateSupport", "Build/WearNTear.UpdateSupport", deep: true),
		new TargetSpec("Smelter", "UpdateSmelter", "Production/Smelter.UpdateSmelter", deep: true),
		new TargetSpec("LootSpawner", "UpdateSpawner", "World/LootSpawner.UpdateSpawner", deep: true),
		new TargetSpec("Heightmap", "CustomLateUpdate", "World/Heightmap.CustomLateUpdate", deep: true),
		new TargetSpec("ClutterSystem", "LateUpdate", "World/ClutterSystem.LateUpdate", deep: true)
	};

	private void Awake()
	{
		//IL_0214: Unknown result type (might be due to invalid IL or missing references)
		//IL_021e: Expected O, but got Unknown
		_log = ((BaseUnityPlugin)this).Logger;
		_serverMode = IsDedicatedServerProcess();
		if (!_serverMode)
		{
			_profilingEnabled = false;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"MagiCorp Valheim Tick Profiler 0.2.5: client process detected; server profiler disabled. Install the companion client overlay from the same package for F8 UI.");
			return;
		}
		_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable collection. Patches remain installed but become a fast no-op when disabled.");
		_reportInterval = ((BaseUnityPlugin)this).Config.Bind<float>("General", "ReportIntervalSeconds", 10f, "Snapshot/report interval in seconds.");
		_topConsumers = ((BaseUnityPlugin)this).Config.Bind<int>("General", "TopConsumers", 12, "Number of consumers written to the BepInEx summary.");
		_deepGameProfiling = ((BaseUnityPlugin)this).Config.Bind<bool>("Profiling", "DeepGameProfiling", true, "Profile per-entity AI, sync, item, building and production hot paths.");
		_profileModPatches = ((BaseUnityPlugin)this).Config.Bind<bool>("Profiling", "ProfileModHarmonyMethods", true, "Profile Harmony Prefix/Postfix/Finalizer methods belonging to other BepInEx plugins.");
		_logPeriodicSummary = ((BaseUnityPlugin)this).Config.Bind<bool>("Output", "LogPeriodicSummary", true, "Write a compact summary to BepInEx/LogOutput.log each interval.");
		_writeStatusJson = ((BaseUnityPlugin)this).Config.Bind<bool>("Output", "WriteStatusJson", true, "Refresh BepInEx/tickprofiler-status.json each interval.");
		_spikeThresholdMs = ((BaseUnityPlugin)this).Config.Bind<float>("Output", "SpikeThresholdMs", 20f, "Mark a consumer as spiking when its worst observed call exceeds this many milliseconds.");
		_profilingEnabled = _enabled.Value;
		_enabled.SettingChanged += delegate
		{
			_profilingEnabled = _enabled.Value;
		};
		_statusPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler-status.json");
		_textPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler-latest.txt");
		_requestPath = Path.Combine(Paths.BepInExRootPath, "tickprofiler.request");
		_process = Process.GetCurrentProcess();
		_lastCpu = _process.TotalProcessorTime;
		_lastGc0 = GC.CollectionCount(0);
		_lastGc1 = GC.CollectionCount(1);
		_lastGc2 = GC.CollectionCount(2);
		_lastReportTicks = Stopwatch.GetTimestamp();
		_lastUpdateTicks = _lastReportTicks;
		_lastFixedTicks = _lastReportTicks;
		_harmony = new Harmony("magicorp.valheim.tickprofiler");
		PatchGameTargets();
		((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1}: patched {2} game methods; deep={3}", "MagiCorp Valheim Tick Profiler", "0.2.5", MethodMetrics.Count, _deepGameProfiling.Value));
	}

	private void Start()
	{
		if (_serverMode)
		{
			TryRegisterProfilerRpc();
			if (_profileModPatches.Value)
			{
				ProfileLoadedMods();
				((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0}: total instrumented methods after mod scan: {1}", "MagiCorp Valheim Tick Profiler", MethodMetrics.Count));
			}
		}
	}

	private void OnDestroy()
	{
		try
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
		catch
		{
		}
	}

	private void Update()
	{
		if (!_serverMode)
		{
			return;
		}
		TryRegisterProfilerRpc();
		if (!_profilingEnabled)
		{
			return;
		}
		long timestamp = Stopwatch.GetTimestamp();
		_updates++;
		if (_lastUpdateTicks != 0L)
		{
			_updateGaps.Add(ToMilliseconds(timestamp - _lastUpdateTicks));
		}
		_lastUpdateTicks = timestamp;
		bool flag = false;
		try
		{
			if (File.Exists(_requestPath))
			{
				File.Delete(_requestPath);
				flag = true;
			}
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Unable to consume tickprofiler.request: " + ex.Message));
		}
		double num = ToSeconds(timestamp - _lastReportTicks);
		double num2 = Math.Max(1.0, _reportInterval.Value);
		if (flag || num >= num2)
		{
			Report(timestamp, flag);
		}
	}

	private void FixedUpdate()
	{
		if (_serverMode && _profilingEnabled)
		{
			long timestamp = Stopwatch.GetTimestamp();
			_fixedUpdates++;
			if (_lastFixedTicks != 0L)
			{
				_fixedGaps.Add(ToMilliseconds(timestamp - _lastFixedTicks));
			}
			_lastFixedTicks = timestamp;
		}
	}

	private void PatchGameTargets()
	{
		TargetSpec[] coreTargets = CoreTargets;
		for (int i = 0; i < coreTargets.Length; i++)
		{
			TargetSpec spec = coreTargets[i];
			if (spec.Deep && !_deepGameProfiling.Value)
			{
				continue;
			}
			Type type = AccessTools.TypeByName(spec.TypeName);
			if (type == null)
			{
				AddPatchFailure(spec.Label + ": type not found");
				continue;
			}
			MethodInfo[] array;
			try
			{
				array = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
					where m.Name == spec.MethodName && !m.IsAbstract && !m.ContainsGenericParameters
					select m).ToArray();
			}
			catch (Exception ex)
			{
				AddPatchFailure(spec.Label + ": enumeration failed: " + ex.Message);
				continue;
			}
			if (array.Length == 0)
			{
				AddPatchFailure(spec.Label + ": method not found");
				continue;
			}
			MethodInfo[] array2 = array;
			foreach (MethodInfo method in array2)
			{
				InstrumentMethod(method, spec.Label, "game");
			}
		}
	}

	private void ProfileLoadedMods()
	{
		foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
		{
			PluginInfo value = pluginInfo.Value;
			if (value == null || (Object)(object)value.Instance == (Object)null)
			{
				continue;
			}
			Assembly assembly = ((object)value.Instance).GetType().Assembly;
			if (assembly == typeof(TickProfilerPlugin).Assembly)
			{
				continue;
			}
			string text = ((value.Metadata != null && !string.IsNullOrEmpty(value.Metadata.Name)) ? value.Metadata.Name : pluginInfo.Key);
			foreach (Type item in SafeGetTypes(assembly))
			{
				if (IsOneShotPatchType(item.Name))
				{
					continue;
				}
				bool flag = HasAttributeNamed(item.GetCustomAttributes(inherit: false), "HarmonyPatch");
				MethodInfo[] methods;
				try
				{
					methods = item.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				}
				catch
				{
					continue;
				}
				MethodInfo[] array = methods;
				foreach (MethodInfo methodInfo in array)
				{
					if (!methodInfo.IsAbstract && !methodInfo.ContainsGenericParameters && methodInfo.GetMethodBody() != null)
					{
						object[] attrs;
						try
						{
							attrs = methodInfo.GetCustomAttributes(inherit: false);
						}
						catch
						{
							attrs = Array.Empty<object>();
						}
						bool num = HasAttributeNamed(attrs, "HarmonyPrefix") || HasAttributeNamed(attrs, "HarmonyPostfix") || HasAttributeNamed(attrs, "HarmonyFinalizer") || (flag && (methodInfo.Name == "Prefix" || methodInfo.Name == "Postfix" || methodInfo.Name == "Finalizer"));
						bool flag2 = typeof(BaseUnityPlugin).IsAssignableFrom(item) && (methodInfo.Name == "Update" || methodInfo.Name == "FixedUpdate" || methodInfo.Name == "LateUpdate");
						if (num || flag2)
						{
							string label = "Mod/" + text + "/" + item.Name + "." + methodInfo.Name;
							InstrumentMethod(methodInfo, label, "mod");
						}
					}
				}
			}
		}
	}

	private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
	{
		try
		{
			return assembly.GetTypes();
		}
		catch (ReflectionTypeLoadException ex)
		{
			return ex.Types.Where((Type t) => t != null);
		}
		catch
		{
			return Array.Empty<Type>();
		}
	}

	private static bool IsOneShotPatchType(string typeName)
	{
		if (string.IsNullOrEmpty(typeName))
		{
			return false;
		}
		if (typeName.IndexOf("Awake", StringComparison.OrdinalIgnoreCase) < 0 && typeName.IndexOf("Start_Patch", StringComparison.OrdinalIgnoreCase) < 0)
		{
			return typeName.IndexOf("OnDestroy", StringComparison.OrdinalIgnoreCase) >= 0;
		}
		return true;
	}

	private static bool HasAttributeNamed(object[] attrs, string name)
	{
		for (int i = 0; i < attrs.Length; i++)
		{
			Type type = attrs[i]?.GetType();
			if (type != null && (type.Name == name || type.FullName == "HarmonyLib." + name))
			{
				return true;
			}
		}
		return false;
	}

	private void InstrumentMethod(MethodBase method, string label, string kind)
	{
		//IL_0046: Unknown result type (might be due to invalid IL or missing references)
		//IL_004b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0057: Expected O, but got Unknown
		//IL_0067: Unknown result type (might be due to invalid IL or missing references)
		//IL_006c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0074: Expected O, but got Unknown
		if (method == null || MethodMetrics.ContainsKey(method))
		{
			return;
		}
		Metric metric = new Metric(label, kind);
		MethodMetrics.Add(method, metric);
		Metrics.Add(metric);
		try
		{
			HarmonyMethod val = new HarmonyMethod(typeof(TickProfilerPlugin), "ProfilePrefix", (Type[])null)
			{
				priority = 800
			};
			HarmonyMethod val2 = new HarmonyMethod(typeof(TickProfilerPlugin), "ProfilePostfix", (Type[])null)
			{
				priority = 0
			};
			_harmony.Patch(method, val, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		}
		catch (Exception ex)
		{
			MethodMetrics.Remove(method);
			Metrics.Remove(metric);
			AddPatchFailure(label + ": " + ex.GetType().Name + " " + ex.Message);
		}
	}

	private static void ProfilePrefix(MethodBase __originalMethod, out int __state)
	{
		__state = -1;
		if (_profilingEnabled && MethodMetrics.TryGetValue(__originalMethod, out var value))
		{
			if (_scopeStack == null)
			{
				_scopeStack = new ScopeFrame[64];
			}
			if (_scopeDepth >= _scopeStack.Length)
			{
				Array.Resize(ref _scopeStack, _scopeStack.Length * 2);
			}
			int num = _scopeDepth++;
			_scopeStack[num].Metric = value;
			_scopeStack[num].StartTicks = Stopwatch.GetTimestamp();
			_scopeStack[num].ChildTicks = 0L;
			__state = num;
		}
	}

	private static void ProfilePostfix(int __state)
	{
		if (__state >= 0 && _scopeStack != null && __state < _scopeStack.Length)
		{
			long num = Stopwatch.GetTimestamp() - _scopeStack[__state].StartTicks;
			long childTicks = _scopeStack[__state].ChildTicks;
			long num2 = num - childTicks;
			if (num2 < 0)
			{
				num2 = 0L;
			}
			_scopeStack[__state].Metric?.Add(num2, num);
			_scopeDepth = __state;
			if (__state > 0)
			{
				_scopeStack[__state - 1].ChildTicks += num;
			}
		}
	}

	private void Report(long nowTicks, bool requested)
	{
		double num = Math.Max(0.001, ToSeconds(nowTicks - _lastReportTicks));
		long updates = _updates;
		long fixedUpdates = _fixedUpdates;
		_updates = 0L;
		_fixedUpdates = 0L;
		_lastReportTicks = nowTicks;
		double updateHz = (double)updates / num;
		double fixedHz = (double)fixedUpdates / num;
		double nominalFixedHz = ((Time.fixedDeltaTime > 0f) ? (1.0 / (double)Time.fixedDeltaTime) : 0.0);
		GapSnapshot updateGap = _updateGaps.SnapshotAndReset();
		GapSnapshot fixedGap = _fixedGaps.SnapshotAndReset();
		TimeSpan totalProcessorTime = _process.TotalProcessorTime;
		double totalMilliseconds = (totalProcessorTime - _lastCpu).TotalMilliseconds;
		_lastCpu = totalProcessorTime;
		double num2 = totalMilliseconds / (num * 1000.0) * 100.0;
		double cpuVmPct = num2 / (double)Math.Max(1, Environment.ProcessorCount);
		int num3 = GC.CollectionCount(0);
		int num4 = GC.CollectionCount(1);
		int num5 = GC.CollectionCount(2);
		int gc = num3 - _lastGc0;
		int gc2 = num4 - _lastGc1;
		int gc3 = num5 - _lastGc2;
		_lastGc0 = num3;
		_lastGc1 = num4;
		_lastGc2 = num5;
		List<MetricSnapshot> list = new List<MetricSnapshot>(Metrics.Count);
		foreach (Metric metric in Metrics)
		{
			MetricRaw raw = metric.SnapshotAndReset();
			if (raw.Calls != 0L)
			{
				list.Add(MetricSnapshot.From(metric, raw, num));
			}
		}
		list.Sort((MetricSnapshot a, MetricSnapshot b) => b.SelfMsPerSec.CompareTo(a.SelfMsPerSec));
		double managedMiB = (double)GC.GetTotalMemory(forceFullCollection: false) / 1048576.0;
		ReportSnapshot s = new ReportSnapshot
		{
			TimestampUtc = DateTime.UtcNow,
			Requested = requested,
			WallSeconds = num,
			UpdateHz = updateHz,
			FixedHz = fixedHz,
			NominalFixedHz = nominalFixedHz,
			FixedDeltaMs = (double)Time.fixedDeltaTime * 1000.0,
			TimeScale = Time.timeScale,
			UpdateGap = updateGap,
			FixedGap = fixedGap,
			CpuOneCorePct = num2,
			CpuVmPct = cpuVmPct,
			ManagedMiB = managedMiB,
			Gc0 = gc,
			Gc1 = gc2,
			Gc2 = gc3,
			Metrics = list,
			PatchFailures = _patchFailures.ToArray(),
			PatchedMethods = MethodMetrics.Count
		};
		string contents = BuildText(s);
		_latestOverlayText = BuildOverlayText(s);
		BroadcastOverlaySnapshot();
		try
		{
			File.WriteAllText(_textPath, contents);
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Unable to write tickprofiler-latest.txt: " + ex.Message));
		}
		if (_writeStatusJson.Value)
		{
			try
			{
				WriteAtomic(_statusPath, BuildJson(s));
			}
			catch (Exception ex2)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Unable to write tickprofiler-status.json: " + ex2.Message));
			}
		}
		if (_logPeriodicSummary.Value || requested)
		{
			string[] array = BuildConsoleText(s).Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string text in array)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)text);
			}
		}
	}

	private static bool IsDedicatedServerProcess()
	{
		try
		{
			return (Process.GetCurrentProcess().ProcessName ?? string.Empty).IndexOf("valheim_server", StringComparison.OrdinalIgnoreCase) >= 0;
		}
		catch
		{
			return false;
		}
	}

	private void TryRegisterProfilerRpc()
	{
		if (_rpcRegistered)
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			return;
		}
		try
		{
			instance.Register("MagiCorp_TickProfiler_Hello_v1", (Action<long>)OnProfilerHello);
			_rpcRegistered = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler client-overlay RPC registered.");
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler RPC registration failed: " + ex.Message));
		}
	}

	private void OnProfilerHello(long sender)
	{
		if (!_serverMode || sender == 0L)
		{
			return;
		}
		if (_overlayPeers.Add(sender))
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("TickProfiler overlay client registered: peer=" + sender + " clients=" + _overlayPeers.Count));
		}
		if (string.IsNullOrEmpty(_latestOverlayText))
		{
			return;
		}
		try
		{
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				instance.InvokeRoutedRPC(sender, "MagiCorp_TickProfiler_Snapshot_v1", new object[1] { _latestOverlayText });
			}
		}
		catch
		{
		}
	}

	private void BroadcastOverlaySnapshot()
	{
		if (string.IsNullOrEmpty(_latestOverlayText) || _overlayPeers.Count == 0)
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		ZNet instance2 = ZNet.instance;
		if (instance == null || (Object)(object)instance2 == (Object)null)
		{
			return;
		}
		long[] array = _overlayPeers.ToArray();
		foreach (long num in array)
		{
			try
			{
				if (instance2.GetPeer(num) == null)
				{
					_overlayPeers.Remove(num);
					continue;
				}
				instance.InvokeRoutedRPC(num, "MagiCorp_TickProfiler_Snapshot_v1", new object[1] { _latestOverlayText });
			}
			catch
			{
				_overlayPeers.Remove(num);
			}
		}
	}

	private string BuildOverlayText(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(2048);
		double value = ((s.NominalFixedHz > 0.0) ? Math.Max(0.0, (s.NominalFixedHz - s.FixedHz) / s.NominalFixedHz * 100.0) : 0.0);
		stringBuilder.Append("SERVER TICK PROFILER\n");
		stringBuilder.Append("Fixed ").Append(F(s.FixedHz, 1)).Append('/')
			.Append(F(s.NominalFixedHz, 1))
			.Append(" Hz   ")
			.Append("Update ")
			.Append(F(s.UpdateHz, 1))
			.Append(" Hz   ")
			.Append("Behind ")
			.Append(F(value, 1))
			.Append("%\n");
		stringBuilder.Append("Fixed gaps: p95 ").Append(F(s.FixedGap.P95Ms, 1)).Append(" ms   worst ")
			.Append(F(s.FixedGap.MaxMs, 1))
			.Append(" ms   >40 ")
			.Append(s.FixedGap.Over40Ms)
			.Append("   >100 ")
			.Append(s.FixedGap.Over100Ms)
			.Append("\n");
		stringBuilder.Append("CPU ").Append(F(s.CpuOneCorePct, 1)).Append("% core   VM ")
			.Append(F(s.CpuVmPct, 1))
			.Append("%   managed ")
			.Append(F(s.ManagedMiB, 0))
			.Append(" MiB   GC ")
			.Append(s.Gc0)
			.Append('/')
			.Append(s.Gc1)
			.Append('/')
			.Append(s.Gc2)
			.Append("\n\n");
		stringBuilder.Append("TOP CONSUMERS\n");
		int num = Math.Min(8, s.Metrics.Count);
		for (int i = 0; i < num; i++)
		{
			MetricSnapshot metricSnapshot = s.Metrics[i];
			stringBuilder.Append(i + 1).Append(". ").Append(metricSnapshot.Name)
				.Append("  ")
				.Append(F(metricSnapshot.SelfMsPerSec, 2))
				.Append(" ms/s")
				.Append("  max ")
				.Append(F(metricSnapshot.MaxInclusiveMs, 1))
				.Append(" ms");
			if (metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value)
			{
				stringBuilder.Append("  SPIKE");
			}
			stringBuilder.Append('\n');
		}
		return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
	}

	private string BuildConsoleText(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(2048);
		double value = ((s.NominalFixedHz > 0.0) ? Math.Max(0.0, (s.NominalFixedHz - s.FixedHz) / s.NominalFixedHz * 100.0) : 0.0);
		stringBuilder.Append("HEALTH  Fixed ").Append(F(s.FixedHz, 1)).Append('/')
			.Append(F(s.NominalFixedHz, 1))
			.Append(" Hz")
			.Append(" | Update ")
			.Append(F(s.UpdateHz, 1))
			.Append(" Hz")
			.Append(" | Lag ")
			.Append(F(value, 1))
			.Append('%')
			.Append(" | CPU ")
			.Append(F(s.CpuOneCorePct, 1))
			.Append("% core")
			.Append(" | RAM ")
			.Append(F(s.ManagedMiB, 0))
			.Append(" MiB")
			.Append(" | GC ")
			.Append(s.Gc0)
			.Append('/')
			.Append(s.Gc1)
			.Append('/')
			.Append(s.Gc2)
			.AppendLine();
		stringBuilder.Append("STALLS  Fixed p95 ").Append(F(s.FixedGap.P95Ms, 1)).Append(" ms")
			.Append(" | worst ")
			.Append(F(s.FixedGap.MaxMs, 1))
			.Append(" ms")
			.Append(" | >40ms ")
			.Append(s.FixedGap.Over40Ms)
			.Append(" | >100ms ")
			.Append(s.FixedGap.Over100Ms)
			.Append(" | Update worst ")
			.Append(F(s.UpdateGap.MaxMs, 1))
			.Append(" ms")
			.AppendLine();
		stringBuilder.Append("TOP CONSUMERS").AppendLine();
		stringBuilder.Append("#  Consumer                                      ms/s   worst   calls/s").AppendLine();
		stringBuilder.Append("-  --------------------------------------------  -----  ------  -------").AppendLine();
		int num = Math.Min(5, s.Metrics.Count);
		for (int i = 0; i < num; i++)
		{
			MetricSnapshot metricSnapshot = s.Metrics[i];
			string text = metricSnapshot.Name ?? "unknown";
			if (text.Length > 44)
			{
				text = text.Substring(0, 41) + "...";
			}
			string text2 = F(metricSnapshot.MaxInclusiveMs, 1) + ((metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value) ? "!" : "");
			stringBuilder.Append((i + 1).ToString(CultureInfo.InvariantCulture)).Append("  ").Append(text.PadRight(44))
				.Append("  ")
				.Append(F(metricSnapshot.SelfMsPerSec, 1).PadLeft(5))
				.Append("  ")
				.Append(text2.PadLeft(6))
				.Append("  ")
				.Append(F(metricSnapshot.CallsPerSec, 1).PadLeft(7))
				.AppendLine();
		}
		if (_patchFailures.Count > 0)
		{
			stringBuilder.Append("WARN  patch failures: ").Append(_patchFailures.Count).Append(" | instrumented: ")
				.Append(s.PatchedMethods)
				.AppendLine();
		}
		return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
	}

	private string BuildText(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(4096);
		double value = ((s.NominalFixedHz > 0.0) ? Math.Max(0.0, (s.NominalFixedHz - s.FixedHz) / s.NominalFixedHz * 100.0) : 0.0);
		stringBuilder.Append("[TickProfiler] ").Append(s.TimestampUtc.ToString("O")).Append(" fixed=")
			.Append(F(s.FixedHz, 1))
			.Append('/')
			.Append(F(s.NominalFixedHz, 1))
			.Append("Hz")
			.Append(" behind=")
			.Append(F(value, 1))
			.Append('%')
			.Append(" update=")
			.Append(F(s.UpdateHz, 1))
			.Append("Hz")
			.Append(" CPU=")
			.Append(F(s.CpuOneCorePct, 1))
			.Append("% of one core")
			.Append(" VMCPU=")
			.Append(F(s.CpuVmPct, 1))
			.Append('%')
			.Append(" managed=")
			.Append(F(s.ManagedMiB, 1))
			.Append("MiB")
			.Append(" GC=")
			.Append(s.Gc0)
			.Append('/')
			.Append(s.Gc1)
			.Append('/')
			.Append(s.Gc2)
			.AppendLine();
		stringBuilder.Append("[TickProfiler] gaps fixed p95=").Append(F(s.FixedGap.P95Ms, 2)).Append("ms worst=")
			.Append(F(s.FixedGap.MaxMs, 2))
			.Append("ms >40ms=")
			.Append(s.FixedGap.Over40Ms)
			.Append(" >100ms=")
			.Append(s.FixedGap.Over100Ms)
			.Append(" | update p95=")
			.Append(F(s.UpdateGap.P95Ms, 2))
			.Append("ms worst=")
			.Append(F(s.UpdateGap.MaxMs, 2))
			.Append("ms")
			.AppendLine();
		int num = Math.Min(Math.Max(1, _topConsumers.Value), s.Metrics.Count);
		for (int i = 0; i < num; i++)
		{
			MetricSnapshot metricSnapshot = s.Metrics[i];
			stringBuilder.Append("[TickProfiler] #").Append(i + 1).Append(' ')
				.Append(metricSnapshot.Name)
				.Append(" self=")
				.Append(F(metricSnapshot.SelfMsPerSec, 3))
				.Append("ms/s")
				.Append(" (")
				.Append(F(metricSnapshot.CpuPct, 3))
				.Append("% core)")
				.Append(" incl=")
				.Append(F(metricSnapshot.InclusiveMsPerSec, 3))
				.Append("ms/s")
				.Append(" calls=")
				.Append(F(metricSnapshot.CallsPerSec, 1))
				.Append("/s")
				.Append(" avg=")
				.Append(F(metricSnapshot.AvgSelfUs, 2))
				.Append("us")
				.Append(" max=")
				.Append(F(metricSnapshot.MaxInclusiveMs, 3))
				.Append("ms");
			if (metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value)
			{
				stringBuilder.Append(" SPIKE");
			}
			stringBuilder.AppendLine();
		}
		if (_patchFailures.Count > 0)
		{
			stringBuilder.Append("[TickProfiler] patchFailures=").Append(_patchFailures.Count).Append(" patchedMethods=")
				.Append(s.PatchedMethods)
				.AppendLine();
		}
		else
		{
			stringBuilder.Append("[TickProfiler] patchedMethods=").Append(s.PatchedMethods).AppendLine();
		}
		return stringBuilder.ToString();
	}

	private string BuildJson(ReportSnapshot s)
	{
		StringBuilder stringBuilder = new StringBuilder(16384);
		stringBuilder.Append('{');
		JsonProp(stringBuilder, "plugin", "MagiCorp Valheim Tick Profiler", first: true);
		JsonProp(stringBuilder, "version", "0.2.5", first: false);
		JsonProp(stringBuilder, "timestampUtc", s.TimestampUtc.ToString("O"), first: false);
		JsonProp(stringBuilder, "requested", s.Requested, first: false);
		JsonProp(stringBuilder, "wallSeconds", s.WallSeconds, first: false);
		JsonProp(stringBuilder, "updateHz", s.UpdateHz, first: false);
		JsonProp(stringBuilder, "fixedHz", s.FixedHz, first: false);
		JsonProp(stringBuilder, "nominalFixedHz", s.NominalFixedHz, first: false);
		JsonProp(stringBuilder, "fixedDeltaMs", s.FixedDeltaMs, first: false);
		JsonProp(stringBuilder, "timeScale", s.TimeScale, first: false);
		JsonProp(stringBuilder, "processCpuOneCorePct", s.CpuOneCorePct, first: false);
		JsonProp(stringBuilder, "processCpuVmPct", s.CpuVmPct, first: false);
		JsonProp(stringBuilder, "managedMemoryMiB", s.ManagedMiB, first: false);
		stringBuilder.Append(",\"gcCollections\":{");
		JsonProp(stringBuilder, "gen0", s.Gc0, first: true);
		JsonProp(stringBuilder, "gen1", s.Gc1, first: false);
		JsonProp(stringBuilder, "gen2", s.Gc2, first: false);
		stringBuilder.Append('}');
		stringBuilder.Append(",\"fixedGap\":");
		AppendGapJson(stringBuilder, s.FixedGap);
		stringBuilder.Append(",\"updateGap\":");
		AppendGapJson(stringBuilder, s.UpdateGap);
		JsonProp(stringBuilder, "patchedMethods", s.PatchedMethods, first: false);
		stringBuilder.Append(",\"patchFailures\":[");
		for (int i = 0; i < s.PatchFailures.Length; i++)
		{
			if (i > 0)
			{
				stringBuilder.Append(',');
			}
			stringBuilder.Append(JsonString(s.PatchFailures[i]));
		}
		stringBuilder.Append(']');
		stringBuilder.Append(",\"consumers\":[");
		int num = Math.Min(Math.Max(_topConsumers.Value, 30), s.Metrics.Count);
		for (int j = 0; j < num; j++)
		{
			if (j > 0)
			{
				stringBuilder.Append(',');
			}
			MetricSnapshot metricSnapshot = s.Metrics[j];
			stringBuilder.Append('{');
			JsonProp(stringBuilder, "name", metricSnapshot.Name, first: true);
			JsonProp(stringBuilder, "kind", metricSnapshot.Kind, first: false);
			JsonProp(stringBuilder, "selfMsPerSec", metricSnapshot.SelfMsPerSec, first: false);
			JsonProp(stringBuilder, "inclusiveMsPerSec", metricSnapshot.InclusiveMsPerSec, first: false);
			JsonProp(stringBuilder, "cpuPctOneCore", metricSnapshot.CpuPct, first: false);
			JsonProp(stringBuilder, "callsPerSec", metricSnapshot.CallsPerSec, first: false);
			JsonProp(stringBuilder, "avgSelfUs", metricSnapshot.AvgSelfUs, first: false);
			JsonProp(stringBuilder, "maxInclusiveMs", metricSnapshot.MaxInclusiveMs, first: false);
			JsonProp(stringBuilder, "spike", metricSnapshot.MaxInclusiveMs >= (double)_spikeThresholdMs.Value, first: false);
			stringBuilder.Append('}');
		}
		stringBuilder.Append("]}");
		return stringBuilder.ToString();
	}

	private static void AppendGapJson(StringBuilder b, GapSnapshot g)
	{
		b.Append('{');
		JsonProp(b, "count", g.Count, first: true);
		JsonProp(b, "avgMs", g.AvgMs, first: false);
		JsonProp(b, "p95Ms", g.P95Ms, first: false);
		JsonProp(b, "maxMs", g.MaxMs, first: false);
		JsonProp(b, "over20Ms", g.Over20Ms, first: false);
		JsonProp(b, "over40Ms", g.Over40Ms, first: false);
		JsonProp(b, "over100Ms", g.Over100Ms, first: false);
		b.Append('}');
	}

	private static void JsonProp(StringBuilder b, string name, string value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(JsonString(value));
	}

	private static void JsonProp(StringBuilder b, string name, bool value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value ? "true" : "false");
	}

	private static void JsonProp(StringBuilder b, string name, int value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value.ToString(CultureInfo.InvariantCulture));
	}

	private static void JsonProp(StringBuilder b, string name, long value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value.ToString(CultureInfo.InvariantCulture));
	}

	private static void JsonProp(StringBuilder b, string name, double value, bool first)
	{
		if (!first)
		{
			b.Append(',');
		}
		b.Append(JsonString(name)).Append(':').Append(value.ToString("0.######", CultureInfo.InvariantCulture));
	}

	private static string JsonString(string value)
	{
		if (value == null)
		{
			return "null";
		}
		StringBuilder stringBuilder = new StringBuilder(value.Length + 8);
		stringBuilder.Append('"');
		foreach (char c in value)
		{
			switch (c)
			{
			case '"':
				stringBuilder.Append("\\\"");
				continue;
			case '\\':
				stringBuilder.Append("\\\\");
				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"));
			}
			else
			{
				stringBuilder.Append(c);
			}
		}
		stringBuilder.Append('"');
		return stringBuilder.ToString();
	}

	private static void WriteAtomic(string path, string content)
	{
		string text = path + ".tmp";
		File.WriteAllText(text, content);
		if (File.Exists(path))
		{
			File.Delete(path);
		}
		File.Move(text, path);
	}

	private void AddPatchFailure(string message)
	{
		_patchFailures.Add(message);
		((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler patch skipped: " + message));
	}

	private static string F(double value, int decimals)
	{
		return value.ToString("F" + decimals, CultureInfo.InvariantCulture);
	}

	private static double ToMilliseconds(long ticks)
	{
		return (double)ticks * 1000.0 / (double)Frequency;
	}

	private static double ToSeconds(long ticks)
	{
		return (double)ticks / (double)Frequency;
	}
}

MagiCorp.ValheimTickProfiler.Client.dll

Decompiled 8 hours ago
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
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.ValheimTickProfiler.Client")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+8a7348b5dbdb8712868f3841af346c25901bdbff")]
[assembly: AssemblyProduct("MagiCorp.ValheimTickProfiler.Client")]
[assembly: AssemblyTitle("MagiCorp.ValheimTickProfiler.Client")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MagiCorp.ValheimTickProfiler.Client;

[BepInPlugin("magicorp.valheim.tickprofiler.client", "MagiCorp Valheim Tick Profiler Client Overlay", "0.2.5")]
[BepInProcess("valheim.exe")]
public sealed class TickProfilerClientOverlay : BaseUnityPlugin
{
	public const string PluginGuid = "magicorp.valheim.tickprofiler.client";

	public const string PluginName = "MagiCorp Valheim Tick Profiler Client Overlay";

	public const string PluginVersion = "0.2.5";

	private const string RpcHello = "MagiCorp_TickProfiler_Hello_v1";

	private const string RpcSnapshot = "MagiCorp_TickProfiler_Snapshot_v1";

	private ConfigEntry<KeyCode> _toggleKey;

	private ConfigEntry<bool> _visibleByDefault;

	private ConfigEntry<int> _fontSize;

	private ConfigEntry<float> _x;

	private ConfigEntry<float> _y;

	private ConfigEntry<float> _width;

	private ConfigEntry<float> _uiScale;

	private bool _visible;

	private bool _rpcRegistered;

	private ZRoutedRpc _registeredRpcInstance;

	private long _lastHelloTicks;

	private string _snapshot = string.Empty;

	private DateTime _snapshotUtc = DateTime.MinValue;

	private GUIStyle _textStyle;

	private bool _loggedFirstSnapshot;

	private bool _loggedPeerIdFailure;

	private static readonly MethodInfo GetServerPeerIdMethod = typeof(ZRoutedRpc).GetMethod("GetServerPeerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	private void Awake()
	{
		//IL_0143: Unknown result type (might be due to invalid IL or missing references)
		_toggleKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Client UI", "ToggleKey", (KeyCode)289, "Show or hide the server TickProfiler overlay.");
		_visibleByDefault = ((BaseUnityPlugin)this).Config.Bind<bool>("Client UI", "VisibleByDefault", false, "Show the overlay when Valheim starts.");
		_fontSize = ((BaseUnityPlugin)this).Config.Bind<int>("Client UI", "FontSize", 14, "Overlay font size.");
		_x = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "X", 16f, "Overlay X position in pixels.");
		_y = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "Y", 16f, "Overlay Y position in pixels.");
		_width = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "Width", 820f, "Overlay width at 1080p before UI scaling.");
		_uiScale = ((BaseUnityPlugin)this).Config.Bind<float>("Client UI", "UIScale", 0f, "Overlay scale. 0 = automatic based on vertical resolution (1x at 1080p, 2x at 4K).");
		_visible = _visibleByDefault.Value;
		((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} {1} loaded in process {2}. Toggle: {3}", "MagiCorp Valheim Tick Profiler Client Overlay", "0.2.5", Process.GetCurrentProcess().ProcessName, _toggleKey.Value));
	}

	private void Update()
	{
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_005d: Unknown result type (might be due to invalid IL or missing references)
		TryRegisterRpc();
		if (TogglePressed())
		{
			_visible = !_visible;
			((BaseUnityPlugin)this).Logger.LogInfo((object)("TickProfiler overlay " + (_visible ? "shown" : "hidden") + " via " + ((object)_toggleKey.Value/*cast due to .constrained prefix*/).ToString() + "."));
		}
		long timestamp = Stopwatch.GetTimestamp();
		if (_rpcRegistered && (double)(timestamp - _lastHelloTicks) / (double)Stopwatch.Frequency >= 10.0)
		{
			SendHello();
		}
	}

	private void TryRegisterRpc()
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			_rpcRegistered = false;
			_registeredRpcInstance = null;
		}
		else if (!_rpcRegistered || _registeredRpcInstance != instance)
		{
			_rpcRegistered = false;
			_registeredRpcInstance = null;
			try
			{
				instance.Register<string>("MagiCorp_TickProfiler_Snapshot_v1", (Action<long, string>)OnSnapshot);
				_registeredRpcInstance = instance;
				_rpcRegistered = true;
				_lastHelloTicks = 0L;
				((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler snapshot RPC registered on current ZRoutedRpc instance.");
				SendHello();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler overlay RPC registration failed: " + ex.Message));
			}
		}
	}

	private void SendHello()
	{
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance == null)
		{
			return;
		}
		long serverPeerId = GetServerPeerId(instance);
		if (serverPeerId == 0L)
		{
			return;
		}
		try
		{
			instance.InvokeRoutedRPC(serverPeerId, "MagiCorp_TickProfiler_Hello_v1", Array.Empty<object>());
			_lastHelloTicks = Stopwatch.GetTimestamp();
			((BaseUnityPlugin)this).Logger.LogDebug((object)("TickProfiler hello sent to server peer " + serverPeerId + "."));
		}
		catch
		{
		}
	}

	private long GetServerPeerId(ZRoutedRpc routed)
	{
		if (routed == null || GetServerPeerIdMethod == null)
		{
			return 0L;
		}
		try
		{
			return (GetServerPeerIdMethod.Invoke(routed, null) is long num) ? num : 0;
		}
		catch (Exception ex)
		{
			if (!_loggedPeerIdFailure)
			{
				_loggedPeerIdFailure = true;
				((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler could not resolve the server peer ID: " + ex.GetBaseException().Message));
			}
			return 0L;
		}
	}

	private void OnSnapshot(long sender, string text)
	{
		if (string.IsNullOrEmpty(text))
		{
			return;
		}
		ZRoutedRpc instance = ZRoutedRpc.instance;
		if (instance != null)
		{
			long serverPeerId = GetServerPeerId(instance);
			if (serverPeerId != 0L && sender != serverPeerId)
			{
				return;
			}
		}
		_snapshot = text;
		_snapshotUtc = DateTime.UtcNow;
		if (!_loggedFirstSnapshot)
		{
			_loggedFirstSnapshot = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TickProfiler first server snapshot received.");
		}
	}

	private bool TogglePressed()
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_001b: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			return ZInput.GetKeyDown(_toggleKey.Value, true);
		}
		catch (Exception ex)
		{
			try
			{
				return Input.GetKeyDown(_toggleKey.Value);
			}
			catch
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("TickProfiler toggle input failed: " + ex.Message));
				return false;
			}
		}
	}

	private void OnGUI()
	{
		//IL_01c2: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c7: Unknown result type (might be due to invalid IL or missing references)
		//IL_0081: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: 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_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a0: Expected O, but got Unknown
		//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00de: Unknown result type (might be due to invalid IL or missing references)
		//IL_028f: Unknown result type (might be due to invalid IL or missing references)
		//IL_02d7: Unknown result type (might be due to invalid IL or missing references)
		if (!_visible)
		{
			return;
		}
		float num = ((_uiScale.Value > 0f) ? Mathf.Clamp(_uiScale.Value, 0.75f, 4f) : Mathf.Clamp((float)Screen.height / 1080f, 1f, 3f));
		int fontSize = Math.Max(10, Mathf.RoundToInt((float)_fontSize.Value * num));
		if (_textStyle == null)
		{
			_textStyle = new GUIStyle(GUI.skin.label)
			{
				richText = false,
				wordWrap = false,
				alignment = (TextAnchor)0
			};
			_textStyle.normal.textColor = Color.white;
		}
		_textStyle.fontSize = fontSize;
		string text;
		if (string.IsNullOrEmpty(_snapshot))
		{
			text = "SERVER TICK PROFILER\nWaiting for TickProfiler data from this server...\nToggle " + ((object)_toggleKey.Value/*cast due to .constrained prefix*/).ToString();
		}
		else
		{
			double num2 = Math.Max(0.0, (DateTime.UtcNow - _snapshotUtc).TotalSeconds);
			float num3 = -1f;
			try
			{
				if ((Object)(object)ZNet.instance != (Object)null)
				{
					num3 = ZNet.instance.GetServerPing() * 1000f;
				}
			}
			catch
			{
			}
			text = _snapshot + "\n\nAge " + num2.ToString("F1", CultureInfo.InvariantCulture) + "s" + ((num3 >= 0f) ? ("   Ping " + num3.ToString("F0", CultureInfo.InvariantCulture) + "ms") : string.Empty) + "   Toggle " + ((object)_toggleKey.Value/*cast due to .constrained prefix*/).ToString();
		}
		int num4 = text.Count((char c) => c == '\n') + 1;
		float num5 = _x.Value * num;
		float num6 = _y.Value * num;
		float val = 520f * num;
		float num7 = Math.Min(Math.Max(val, _width.Value * num), Math.Max(val, (float)Screen.width - num5 - 12f * num));
		float num8 = 20f * num + (float)num4 * ((float)_textStyle.fontSize + 5f * num);
		Rect val2 = default(Rect);
		((Rect)(ref val2))..ctor(num5, num6, num7, num8);
		GUI.Box(val2, GUIContent.none);
		GUI.Label(new Rect(((Rect)(ref val2)).x + 10f * num, ((Rect)(ref val2)).y + 8f * num, ((Rect)(ref val2)).width - 20f * num, ((Rect)(ref val2)).height - 16f * num), text, _textStyle);
	}
}