Decompiled source of TheGreatHall v3.0.51

Deathboard.Client.dll

Decompiled a month ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Splatform;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("Deathboard.Client")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Client companion for Deathboard realm servers. Inert everywhere else.")]
[assembly: AssemblyFileVersion("3.0.0.0")]
[assembly: AssemblyInformationalVersion("3.0.0+50a58f88d65ce17edb1ecef1547196a134bc4c9c")]
[assembly: AssemblyProduct("Deathboard.Client")]
[assembly: AssemblyTitle("Deathboard.Client")]
[assembly: AssemblyVersion("3.0.0.0")]
namespace Deathboard
{
	internal struct BoardRow
	{
		public string Name;

		public int Deaths;

		public bool HasSkills;

		public float Combat;

		public float Acro;

		public float Hand;

		public float Power;

		public bool Online;
	}
	internal struct BoardRequest
	{
		public int Protocol;

		public byte Kind;

		public byte Sort;
	}
	internal struct BossTick
	{
		public string Key;

		public string Name;

		public bool Down;
	}
	internal sealed class BoardView
	{
		public int Protocol;

		public byte Kind;

		public byte Sort;

		public string Title = "";

		public string[] Labels = new string[3] { "Combat", "Agility", "Trades" };

		public bool ShowPower = true;

		public string Formula = "";

		public int TotalDeaths;

		public int WorldDay;

		public List<BoardRow> Rows = new List<BoardRow>();

		public List<BossTick> Bosses = new List<BossTick>();
	}
	internal static class BoardPayload
	{
		internal static class BoardKindWire
		{
			public const byte Deaths = 0;

			public const byte Power = 1;

			public const byte Combat = 2;

			public const byte Acro = 3;

			public const byte Hand = 4;

			public const byte Who = 5;

			public const byte Count = 6;
		}

		public const int MaxBosses = 32;

		public const int MaxRows = 200;

		public const int MaxNameChars = 64;

		public const byte SortAscending = 0;

		public const byte SortDescending = 1;

		public static ZPackage WriteRequest(byte kind, byte sort)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(kind);
			val.Write((sort != 0) ? ((byte)1) : ((byte)0));
			return val;
		}

		public static bool TryReadRequest(ZPackage pkg, out BoardRequest request)
		{
			request = default(BoardRequest);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				request.Protocol = pkg.ReadInt();
				request.Kind = pkg.ReadByte();
				request.Sort = ((pkg.ReadByte() != 0) ? ((byte)1) : ((byte)0));
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteBoard(byte kind, byte sort, string title, string[] labels, bool showPower, string formula, int totalDeaths, int worldDay, IList<BoardRow> rows, IList<BossTick> bosses)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(kind);
			val.Write((sort != 0) ? ((byte)1) : ((byte)0));
			val.Write(title ?? "");
			string[] array = ((labels != null && labels.Length == 3) ? labels : new string[3] { "Combat", "Agility", "Trades" });
			val.Write(array[0] ?? "");
			val.Write(array[1] ?? "");
			val.Write(array[2] ?? "");
			val.Write(showPower);
			val.Write(formula ?? "");
			val.Write(totalDeaths);
			val.Write(worldDay);
			int num = ((rows != null) ? Math.Min(rows.Count, 200) : 0);
			val.Write(num);
			for (int i = 0; i < num; i++)
			{
				BoardRow boardRow = rows[i];
				val.Write(Trim(boardRow.Name));
				val.Write(boardRow.Deaths);
				val.Write(boardRow.HasSkills);
				val.Write(boardRow.Combat);
				val.Write(boardRow.Acro);
				val.Write(boardRow.Hand);
				val.Write(boardRow.Power);
				val.Write(boardRow.Online);
			}
			int num2 = ((bosses != null) ? Math.Min(bosses.Count, 32) : 0);
			val.Write(num2);
			for (int j = 0; j < num2; j++)
			{
				BossTick bossTick = bosses[j];
				val.Write(Trim(bossTick.Key));
				val.Write(Trim(bossTick.Name));
				val.Write(bossTick.Down);
			}
			return val;
		}

		public static bool TryReadBoard(ZPackage pkg, out BoardView view)
		{
			view = null;
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				BoardView boardView = new BoardView();
				boardView.Protocol = pkg.ReadInt();
				boardView.Kind = pkg.ReadByte();
				boardView.Sort = ((pkg.ReadByte() != 0) ? ((byte)1) : ((byte)0));
				boardView.Title = pkg.ReadString() ?? "";
				boardView.Labels = new string[3]
				{
					pkg.ReadString() ?? "Combat",
					pkg.ReadString() ?? "Agility",
					pkg.ReadString() ?? "Trades"
				};
				boardView.ShowPower = pkg.ReadBool();
				boardView.Formula = pkg.ReadString() ?? "";
				boardView.TotalDeaths = pkg.ReadInt();
				boardView.WorldDay = pkg.ReadInt();
				int num = pkg.ReadInt();
				if (num < 0 || num > 200)
				{
					return false;
				}
				boardView.Rows = new List<BoardRow>(num);
				for (int i = 0; i < num; i++)
				{
					BoardRow item = new BoardRow
					{
						Name = Trim(pkg.ReadString()),
						Deaths = pkg.ReadInt(),
						HasSkills = pkg.ReadBool(),
						Combat = pkg.ReadSingle(),
						Acro = pkg.ReadSingle(),
						Hand = pkg.ReadSingle(),
						Power = pkg.ReadSingle(),
						Online = pkg.ReadBool()
					};
					boardView.Rows.Add(item);
				}
				int num2 = pkg.ReadInt();
				if (num2 < 0 || num2 > 32)
				{
					return false;
				}
				boardView.Bosses = new List<BossTick>(num2);
				for (int j = 0; j < num2; j++)
				{
					BossTick item2 = new BossTick
					{
						Key = Trim(pkg.ReadString()),
						Name = Trim(pkg.ReadString()),
						Down = pkg.ReadBool()
					};
					boardView.Bosses.Add(item2);
				}
				view = boardView;
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static string Trim(string name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return "?";
			}
			return DeathCause.Sanitize(name, 64);
		}
	}
	internal static class BuildInfo
	{
		public const string ServerName = "Deathboard";

		public const string ServerGuid = "com.deathboard.valheim";

		public const string ClientName = "Deathboard.Client";

		public const string ClientGuid = "com.deathboard.valheim.client";

		public const string DisplayName = "The Great Hall";

		public const string Author = "Zomax";

		public const string Version = "3.0.0";

		public const int Build = 51;

		public static string ServerBanner => "Deathboard v3.0.0 build " + 51;

		public static string ClientBanner => "Deathboard.Client v3.0.0 build " + 51;

		public static string PublicBanner => "The Great Hall v3.0.0 build " + 51;
	}
	internal static class DeathCause
	{
		public const int MaxKeyChars = 64;

		public const char EnvPrefix = '#';

		public const char PlayerPrefix = '@';

		private static readonly string[] HitTypes = new string[25]
		{
			"Undefined", "EnemyHit", "PlayerHit", "Fall", "Drowning", "Burning", "Freezing", "Poisoned", "Water", "Smoke",
			"EdgeOfWorld", "Impact", "Cart", "Tree", "Self", "Structural", "Turret", "Boat", "Stalagtite", "Catapult",
			"CinderFire", "AshlandsOcean", "AshlandsLava", "Incinerator", "DrawBridge"
		};

		private static readonly Dictionary<string, string> EnvLabels = new Dictionary<string, string>(StringComparer.Ordinal)
		{
			{ "Undefined", "Unknown" },
			{ "Fall", "Falling" },
			{ "Drowning", "Drowning" },
			{ "Burning", "Fire" },
			{ "Freezing", "Cold" },
			{ "Poisoned", "Poison" },
			{ "Water", "Deep Water" },
			{ "Smoke", "Smoke" },
			{ "EdgeOfWorld", "Edge of the World" },
			{ "Impact", "Impact" },
			{ "Cart", "Cart" },
			{ "Tree", "Falling Tree" },
			{ "Self", "Own Hand" },
			{ "Structural", "Collapsing Structure" },
			{ "Turret", "Turret" },
			{ "Boat", "Boat" },
			{ "Stalagtite", "Stalagmite" },
			{ "Catapult", "Catapult" },
			{ "CinderFire", "Cinder Fire" },
			{ "AshlandsOcean", "Ashlands Ocean" },
			{ "AshlandsLava", "Ashlands Lava" },
			{ "Incinerator", "Incinerator" },
			{ "DrawBridge", "Drawbridge" }
		};

		private static readonly Dictionary<string, string> EnvVerbs = new Dictionary<string, string>(StringComparer.Ordinal)
		{
			{ "Undefined", "died of unknown causes" },
			{ "Fall", "fell to their death" },
			{ "Drowning", "drowned" },
			{ "Burning", "burned to death" },
			{ "Freezing", "froze to death" },
			{ "Poisoned", "succumbed to poison" },
			{ "Water", "was lost to the deep" },
			{ "Smoke", "choked on smoke" },
			{ "EdgeOfWorld", "sailed off the edge of the world" },
			{ "Impact", "died on impact" },
			{ "Cart", "was run down by a cart" },
			{ "Tree", "was crushed by a falling tree" },
			{ "Self", "died by their own hand" },
			{ "Structural", "was crushed by a collapsing structure" },
			{ "Turret", "was shot down by a turret" },
			{ "Boat", "was run down by a boat" },
			{ "Stalagtite", "was impaled by a stalagmite" },
			{ "Catapult", "was struck by a catapult" },
			{ "CinderFire", "burned in cinder fire" },
			{ "AshlandsOcean", "was claimed by the Ashlands ocean" },
			{ "AshlandsLava", "sank into the lava" },
			{ "Incinerator", "went into the incinerator" },
			{ "DrawBridge", "was crushed by a drawbridge" }
		};

		public static int HitTypeCount => HitTypes.Length;

		public static string HitTypeName(int hitType)
		{
			if (hitType < 0 || hitType >= HitTypes.Length)
			{
				return HitTypes[0];
			}
			return HitTypes[hitType];
		}

		public static string EnvKey(int hitType)
		{
			return "#" + HitTypeName(hitType);
		}

		public static bool IsEnvironmental(string key)
		{
			if (!string.IsNullOrEmpty(key))
			{
				return key[0] == '#';
			}
			return false;
		}

		public static bool IsPlayer(string key)
		{
			if (!string.IsNullOrEmpty(key))
			{
				return key[0] == '@';
			}
			return false;
		}

		public static string KeyFor(int hitType, string attackerKey, bool attackerIsPlayer)
		{
			string text = Trim(attackerKey);
			if (string.IsNullOrEmpty(text))
			{
				return EnvKey(hitType);
			}
			if (text[0] == '@' || text[0] == '#')
			{
				return text;
			}
			if (!attackerIsPlayer)
			{
				return text;
			}
			return "@" + text;
		}

		public static string ShortLabel(string key, string display)
		{
			if (string.IsNullOrEmpty(key))
			{
				return "Unknown";
			}
			if (IsPlayer(key))
			{
				return key.Substring(1);
			}
			if (!IsEnvironmental(key))
			{
				string text = Trim(display);
				if (!string.IsNullOrEmpty(text))
				{
					return text;
				}
				return Trim(key);
			}
			string text2 = key.Substring(1);
			if (!EnvLabels.TryGetValue(text2, out var value))
			{
				return Prettify(text2);
			}
			return value;
		}

		public static string Category(string key)
		{
			if (string.IsNullOrEmpty(key))
			{
				return "Unknown";
			}
			if (IsPlayer(key))
			{
				return "Viking";
			}
			if (!IsEnvironmental(key))
			{
				return "Creature";
			}
			switch (key.Substring(1))
			{
			case "EnemyHit":
			case "PlayerHit":
				return "Combat";
			case "Catapult":
			case "Fall":
			case "Cart":
			case "Tree":
			case "Boat":
			case "Self":
			case "Impact":
			case "Turret":
			case "Stalagtite":
			case "Structural":
				return "Mishap";
			case "Drowning":
			case "Freezing":
			case "Poisoned":
			case "CinderFire":
			case "Smoke":
			case "Water":
			case "Burning":
			case "EdgeOfWorld":
			case "AshlandsOcean":
				return "Environment";
			default:
				return "Unknown";
			}
		}

		public static string Sentence(string subject, string key, string display)
		{
			string text = (string.IsNullOrEmpty(subject) ? "?" : subject);
			if (string.IsNullOrEmpty(key))
			{
				return text + " has died";
			}
			if (IsPlayer(key))
			{
				return text + " was slain by " + key.Substring(1);
			}
			if (!IsEnvironmental(key))
			{
				return text + " was killed by " + Article(ShortLabel(key, display));
			}
			string text2 = key.Substring(1);
			if (text2 == "EnemyHit")
			{
				return text + " was killed in battle";
			}
			if (text2 == "PlayerHit")
			{
				return text + " was killed by another Viking";
			}
			if (!EnvVerbs.TryGetValue(text2, out var value))
			{
				return text + " has died";
			}
			return text + " " + value;
		}

		private static string Article(string noun)
		{
			if (string.IsNullOrEmpty(noun))
			{
				return "something";
			}
			if (noun.StartsWith("a ", StringComparison.OrdinalIgnoreCase) || noun.StartsWith("an ", StringComparison.OrdinalIgnoreCase) || noun.StartsWith("the ", StringComparison.OrdinalIgnoreCase))
			{
				return noun;
			}
			char c = char.ToLowerInvariant(noun[0]);
			return ((c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') ? "an " : "a ") + noun;
		}

		private static string Prettify(string name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return "Unknown";
			}
			StringBuilder stringBuilder = new StringBuilder(name.Length + 8);
			for (int i = 0; i < name.Length; i++)
			{
				if (i > 0 && char.IsUpper(name[i]) && !char.IsUpper(name[i - 1]))
				{
					stringBuilder.Append(' ');
				}
				stringBuilder.Append(name[i]);
			}
			return stringBuilder.ToString();
		}

		public static string Trim(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "";
			}
			return Sanitize(value, 64).Trim();
		}

		public static string Sanitize(string value, int maxChars)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "";
			}
			int num = Math.Min(value.Length, maxChars);
			char[] array = new char[num];
			for (int i = 0; i < num; i++)
			{
				char c = value[i];
				array[i] = ((c < ' ' || c == '\u007f') ? ' ' : c);
			}
			return new string(array);
		}
	}
	internal sealed class FchAppearance
	{
		public string Beard = "";

		public string Hair = "";

		public float SkinR = 1f;

		public float SkinG = 1f;

		public float SkinB = 1f;

		public float HairR = 1f;

		public float HairG = 1f;

		public float HairB = 1f;

		public int ModelIndex;
	}
	internal static class FchAppearanceReader
	{
		private sealed class Cursor
		{
			private readonly byte[] _data;

			private int _at;

			public Cursor(byte[] data)
			{
				_data = data;
			}

			private void Need(int bytes)
			{
				if (bytes < 0 || _at + bytes > _data.Length)
				{
					throw new FormatException("past the end");
				}
			}

			public void Skip(int bytes)
			{
				Need(bytes);
				_at += bytes;
			}

			public int Int()
			{
				Need(4);
				int result = BitConverter.ToInt32(_data, _at);
				_at += 4;
				return result;
			}

			public float Float()
			{
				Need(4);
				float result = BitConverter.ToSingle(_data, _at);
				_at += 4;
				return result;
			}

			private int StringLength()
			{
				int num = 0;
				int num2 = 0;
				byte b;
				do
				{
					if (num2 >= 35)
					{
						throw new FormatException("bad string length");
					}
					Need(1);
					b = _data[_at++];
					num |= (b & 0x7F) << num2;
					num2 += 7;
				}
				while ((b & 0x80) != 0);
				if (num < 0 || num > 1024)
				{
					throw new FormatException("string too long");
				}
				return num;
			}

			public string String()
			{
				int num = StringLength();
				Need(num);
				string result = Encoding.UTF8.GetString(_data, _at, num);
				_at += num;
				return result;
			}

			public bool SkipString()
			{
				Skip(StringLength());
				return true;
			}
		}

		public const int PlayerSaveVersion = 29;

		public const int InventorySaveVersion = 106;

		private const int MaxCount = 65536;

		private const int MaxStringBytes = 1024;

		private const int MaxModelIndex = 16;

		private const float MaxColour = 4f;

		public static FchAppearance Read(byte[] data)
		{
			if (data == null || data.Length < 8)
			{
				return null;
			}
			Cursor cursor = new Cursor(data);
			try
			{
				if (cursor.Int() != 29)
				{
					return null;
				}
				cursor.Skip(16);
				if (!cursor.SkipString())
				{
					return null;
				}
				cursor.Skip(4);
				if (!SkipInventory(cursor))
				{
					return null;
				}
				if (!SkipStrings(cursor))
				{
					return null;
				}
				if (!SkipStringIntPairs(cursor))
				{
					return null;
				}
				if (!SkipStrings(cursor))
				{
					return null;
				}
				if (!SkipStrings(cursor))
				{
					return null;
				}
				if (!SkipStrings(cursor))
				{
					return null;
				}
				if (!SkipStrings(cursor))
				{
					return null;
				}
				if (!SkipInts(cursor))
				{
					return null;
				}
				if (!SkipStringPairs(cursor))
				{
					return null;
				}
				FchAppearance fchAppearance = new FchAppearance();
				fchAppearance.Beard = cursor.String();
				fchAppearance.Hair = cursor.String();
				if (fchAppearance.Beard == null || fchAppearance.Hair == null)
				{
					return null;
				}
				fchAppearance.SkinR = cursor.Float();
				fchAppearance.SkinG = cursor.Float();
				fchAppearance.SkinB = cursor.Float();
				fchAppearance.HairR = cursor.Float();
				fchAppearance.HairG = cursor.Float();
				fchAppearance.HairB = cursor.Float();
				fchAppearance.ModelIndex = cursor.Int();
				return Plausible(fchAppearance) ? fchAppearance : null;
			}
			catch (Exception)
			{
				return null;
			}
		}

		private static bool Plausible(FchAppearance look)
		{
			if (look.ModelIndex < 0 || look.ModelIndex > 16)
			{
				return false;
			}
			float[] array = new float[6] { look.SkinR, look.SkinG, look.SkinB, look.HairR, look.HairG, look.HairB };
			foreach (float num in array)
			{
				if (float.IsNaN(num) || num < 0f || num > 4f)
				{
					return false;
				}
			}
			if (NameLike(look.Beard))
			{
				return NameLike(look.Hair);
			}
			return false;
		}

		private static bool NameLike(string value)
		{
			if (value == null)
			{
				return false;
			}
			if (value.Length == 0)
			{
				return true;
			}
			if (value.Length > 64)
			{
				return false;
			}
			foreach (char c in value)
			{
				if (!char.IsLetterOrDigit(c) && c != '_' && c != '-')
				{
					return false;
				}
			}
			return true;
		}

		private static bool SkipInventory(Cursor c)
		{
			if (c.Int() != 106)
			{
				return false;
			}
			int num = c.Int();
			if (num < 0 || num > 65536)
			{
				return false;
			}
			for (int i = 0; i < num; i++)
			{
				if (!c.SkipString())
				{
					return false;
				}
				c.Skip(4);
				c.Skip(4);
				c.Skip(8);
				c.Skip(1);
				c.Skip(4);
				c.Skip(4);
				c.Skip(8);
				if (!c.SkipString())
				{
					return false;
				}
				if (!SkipStringPairs(c))
				{
					return false;
				}
				c.Skip(4);
				c.Skip(1);
			}
			return true;
		}

		private static bool SkipStrings(Cursor c)
		{
			int num = c.Int();
			if (num < 0 || num > 65536)
			{
				return false;
			}
			for (int i = 0; i < num; i++)
			{
				if (!c.SkipString())
				{
					return false;
				}
			}
			return true;
		}

		private static bool SkipInts(Cursor c)
		{
			int num = c.Int();
			if (num < 0 || num > 65536)
			{
				return false;
			}
			c.Skip(num * 4);
			return true;
		}

		private static bool SkipStringPairs(Cursor c)
		{
			int num = c.Int();
			if (num < 0 || num > 65536)
			{
				return false;
			}
			for (int i = 0; i < num; i++)
			{
				if (!c.SkipString())
				{
					return false;
				}
				if (!c.SkipString())
				{
					return false;
				}
			}
			return true;
		}

		private static bool SkipStringIntPairs(Cursor c)
		{
			int num = c.Int();
			if (num < 0 || num > 65536)
			{
				return false;
			}
			for (int i = 0; i < num; i++)
			{
				if (!c.SkipString())
				{
					return false;
				}
				c.Skip(4);
			}
			return true;
		}
	}
	internal static class FchSkillReader
	{
		private const int SkillsSaveVersion = 2;

		private const int MaxSkills = 64;

		private const float MaxLevel = 100f;

		private const float MaxAccumulator = 600f;

		public static Dictionary<int, float> FindSkillBlock(byte[] data, Func<int, bool> isValidSkillId)
		{
			if (data == null || data.Length < 8 || isValidSkillId == null)
			{
				return null;
			}
			Dictionary<int, float> result = null;
			int num = -1;
			for (int i = 0; i + 8 <= data.Length; i++)
			{
				if (BitConverter.ToInt32(data, i) != 2)
				{
					continue;
				}
				int num2 = BitConverter.ToInt32(data, i + 4);
				if (num2 < 1 || num2 > 64)
				{
					continue;
				}
				int num3 = i + 8 + num2 * 12;
				if (num3 > data.Length)
				{
					continue;
				}
				Dictionary<int, float> dictionary = ReadTriples(data, i + 8, num2, isValidSkillId);
				if (dictionary == null)
				{
					continue;
				}
				int num4 = num2 * 2;
				if (num3 + 4 <= data.Length)
				{
					int num5 = BitConverter.ToInt32(data, num3);
					if (num5 >= 0 && num5 <= 65536)
					{
						num4++;
					}
				}
				if (num4 > num)
				{
					num = num4;
					result = dictionary;
				}
			}
			return result;
		}

		private static Dictionary<int, float> ReadTriples(byte[] data, int offset, int count, Func<int, bool> isValidSkillId)
		{
			Dictionary<int, float> dictionary = new Dictionary<int, float>(count);
			for (int i = 0; i < count; i++)
			{
				int num = offset + i * 12;
				int num2 = BitConverter.ToInt32(data, num);
				if (!isValidSkillId(num2))
				{
					return null;
				}
				if (dictionary.ContainsKey(num2))
				{
					return null;
				}
				float num3 = BitConverter.ToSingle(data, num + 4);
				if (float.IsNaN(num3) || num3 < 0f || num3 > 100f)
				{
					return null;
				}
				float num4 = BitConverter.ToSingle(data, num + 8);
				if (float.IsNaN(num4) || num4 < 0f || num4 > 600f)
				{
					return null;
				}
				dictionary[num2] = num3;
			}
			return dictionary;
		}
	}
	internal static class MiniJson
	{
		public static void WriteString(StringBuilder sb, string value)
		{
			if (value == null)
			{
				sb.Append("null");
				return;
			}
			sb.Append('"');
			foreach (char c in value)
			{
				switch (c)
				{
				case '"':
					sb.Append("\\\"");
					continue;
				case '\\':
					sb.Append("\\\\");
					continue;
				case '\b':
					sb.Append("\\b");
					continue;
				case '\f':
					sb.Append("\\f");
					continue;
				case '\n':
					sb.Append("\\n");
					continue;
				case '\r':
					sb.Append("\\r");
					continue;
				case '\t':
					sb.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder = sb.Append("\\u");
					int num = c;
					stringBuilder.Append(num.ToString("x4", CultureInfo.InvariantCulture));
				}
				else
				{
					sb.Append(c);
				}
			}
			sb.Append('"');
		}

		public static object Parse(string json)
		{
			int i = 0;
			object result = ParseValue(json, ref i);
			SkipWhitespace(json, ref i);
			return result;
		}

		public static Dictionary<string, object> AsObject(object value)
		{
			return value as Dictionary<string, object>;
		}

		public static List<object> AsArray(object value)
		{
			return value as List<object>;
		}

		public static string GetString(Dictionary<string, object> obj, string key, string fallback = null)
		{
			if (obj != null && obj.TryGetValue(key, out var value) && value is string result)
			{
				return result;
			}
			return fallback;
		}

		public static int GetInt(Dictionary<string, object> obj, string key, int fallback = 0)
		{
			if (obj != null && obj.TryGetValue(key, out var value))
			{
				if (value is double a)
				{
					return (int)Math.Round(a);
				}
				if (value is string s && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
				{
					return result;
				}
			}
			return fallback;
		}

		public static float GetFloat(Dictionary<string, object> obj, string key, float fallback = 0f)
		{
			if (obj != null && obj.TryGetValue(key, out var value))
			{
				if (value is double num)
				{
					return (float)num;
				}
				if (value is string s && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					return result;
				}
			}
			return fallback;
		}

		public static bool GetBool(Dictionary<string, object> obj, string key, bool fallback = false)
		{
			if (obj != null && obj.TryGetValue(key, out var value))
			{
				if (value is bool)
				{
					return (bool)value;
				}
				if (value is string value2 && bool.TryParse(value2, out var result))
				{
					return result;
				}
				if (value is double value3)
				{
					return Math.Abs(value3) > double.Epsilon;
				}
			}
			return fallback;
		}

		private static object ParseValue(string s, ref int i)
		{
			SkipWhitespace(s, ref i);
			if (i >= s.Length)
			{
				throw new FormatException("Unexpected end of JSON input.");
			}
			switch (s[i])
			{
			case '{':
				return ParseObject(s, ref i);
			case '[':
				return ParseArray(s, ref i);
			case '"':
				return ParseString(s, ref i);
			case 't':
				Expect(s, ref i, "true");
				return true;
			case 'f':
				Expect(s, ref i, "false");
				return false;
			case 'n':
				Expect(s, ref i, "null");
				return null;
			default:
				return ParseNumber(s, ref i);
			}
		}

		private static Dictionary<string, object> ParseObject(string s, ref int i)
		{
			Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
			i++;
			SkipWhitespace(s, ref i);
			if (i < s.Length && s[i] == '}')
			{
				i++;
				return dictionary;
			}
			while (true)
			{
				SkipWhitespace(s, ref i);
				if (i >= s.Length || s[i] != '"')
				{
					throw new FormatException("Expected an object key at offset " + i + ".");
				}
				string key = ParseString(s, ref i);
				SkipWhitespace(s, ref i);
				if (i >= s.Length || s[i] != ':')
				{
					throw new FormatException("Expected ':' at offset " + i + ".");
				}
				i++;
				dictionary[key] = ParseValue(s, ref i);
				SkipWhitespace(s, ref i);
				if (i >= s.Length)
				{
					throw new FormatException("Unterminated object.");
				}
				if (s[i] != ',')
				{
					break;
				}
				i++;
			}
			if (s[i] == '}')
			{
				i++;
				return dictionary;
			}
			throw new FormatException("Expected ',' or '}' at offset " + i + ".");
		}

		private static List<object> ParseArray(string s, ref int i)
		{
			List<object> list = new List<object>();
			i++;
			SkipWhitespace(s, ref i);
			if (i < s.Length && s[i] == ']')
			{
				i++;
				return list;
			}
			while (true)
			{
				list.Add(ParseValue(s, ref i));
				SkipWhitespace(s, ref i);
				if (i >= s.Length)
				{
					throw new FormatException("Unterminated array.");
				}
				if (s[i] != ',')
				{
					break;
				}
				i++;
			}
			if (s[i] == ']')
			{
				i++;
				return list;
			}
			throw new FormatException("Expected ',' or ']' at offset " + i + ".");
		}

		private static string ParseString(string s, ref int i)
		{
			StringBuilder stringBuilder = new StringBuilder();
			i++;
			while (i < s.Length)
			{
				char c = s[i++];
				switch (c)
				{
				case '"':
					return stringBuilder.ToString();
				default:
					stringBuilder.Append(c);
					continue;
				case '\\':
					break;
				}
				if (i >= s.Length)
				{
					break;
				}
				char c2 = s[i++];
				switch (c2)
				{
				case '"':
					stringBuilder.Append('"');
					break;
				case '\\':
					stringBuilder.Append('\\');
					break;
				case '/':
					stringBuilder.Append('/');
					break;
				case 'b':
					stringBuilder.Append('\b');
					break;
				case 'f':
					stringBuilder.Append('\f');
					break;
				case 'n':
					stringBuilder.Append('\n');
					break;
				case 'r':
					stringBuilder.Append('\r');
					break;
				case 't':
					stringBuilder.Append('\t');
					break;
				case 'u':
					if (i + 4 > s.Length)
					{
						throw new FormatException("Truncated unicode escape.");
					}
					stringBuilder.Append((char)ushort.Parse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
					i += 4;
					break;
				default:
					throw new FormatException("Unknown escape character '" + c2 + "'.");
				}
			}
			throw new FormatException("Unterminated string.");
		}

		private static double ParseNumber(string s, ref int i)
		{
			int num = i;
			while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0)
			{
				i++;
			}
			string text = s.Substring(num, i - num);
			if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
			{
				throw new FormatException("Invalid number '" + text + "'.");
			}
			return result;
		}

		private static void Expect(string s, ref int i, string literal)
		{
			if (i + literal.Length > s.Length || string.CompareOrdinal(s, i, literal, 0, literal.Length) != 0)
			{
				throw new FormatException("Expected '" + literal + "' at offset " + i + ".");
			}
			i += literal.Length;
		}

		private static void SkipWhitespace(string s, ref int i)
		{
			while (i < s.Length && char.IsWhiteSpace(s[i]))
			{
				i++;
			}
		}
	}
	internal static class PatchGuard
	{
		public static Action<string> Log;

		private static readonly Dictionary<string, bool> Disabled = new Dictionary<string, bool>(StringComparer.Ordinal);

		private static readonly object Lock = new object();

		public static int DisabledCount
		{
			get
			{
				lock (Lock)
				{
					return Disabled.Count;
				}
			}
		}

		public static bool Run(string patchName, Action body)
		{
			return Run(patchName, body, Log);
		}

		public static bool Run(string patchName, Action body, Action<string> log)
		{
			if (body == null)
			{
				return false;
			}
			string patchName2 = (string.IsNullOrEmpty(patchName) ? "<unnamed patch>" : patchName);
			if (IsDisabled(patchName2))
			{
				return false;
			}
			try
			{
				body();
				return true;
			}
			catch (Exception ex)
			{
				Disable(patchName2);
				Report(log, patchName2, ex);
				return false;
			}
		}

		public static bool IsDisabled(string patchName)
		{
			if (string.IsNullOrEmpty(patchName))
			{
				return false;
			}
			lock (Lock)
			{
				bool value;
				return Disabled.TryGetValue(patchName, out value) && value;
			}
		}

		public static void Reset()
		{
			lock (Lock)
			{
				Disabled.Clear();
			}
		}

		private static void Disable(string patchName)
		{
			lock (Lock)
			{
				Disabled[patchName] = true;
			}
		}

		private static void Report(Action<string> log, string patchName, Exception ex)
		{
			if (log == null)
			{
				return;
			}
			try
			{
				log("PATCH DISABLED: " + patchName + " threw and has been switched off for this session. The game is unaffected; the feature behind it is not running. " + ex);
			}
			catch (Exception)
			{
			}
		}
	}
	internal struct SkillProfile
	{
		public bool HasData;

		public float Combat;

		public float Acro;

		public float Hand;

		public static readonly SkillProfile Unknown;
	}
	internal sealed class PowerWeights
	{
		public float Combat = 0.5f;

		public float Acro = 0.25f;

		public float Hand = 0.25f;

		public static readonly PowerWeights Default = new PowerWeights();

		public static PowerWeights Parse(string value, out string error)
		{
			error = null;
			if (string.IsNullOrEmpty(value))
			{
				return Default;
			}
			string[] array = value.Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length != 3)
			{
				error = "expected three comma-separated numbers, got \"" + value + "\"";
				return Default;
			}
			PowerWeights powerWeights = new PowerWeights();
			if (!float.TryParse(array[0], NumberStyles.Float, CultureInfo.InvariantCulture, out powerWeights.Combat) || !float.TryParse(array[1], NumberStyles.Float, CultureInfo.InvariantCulture, out powerWeights.Acro) || !float.TryParse(array[2], NumberStyles.Float, CultureInfo.InvariantCulture, out powerWeights.Hand))
			{
				error = "could not parse \"" + value + "\" as three numbers";
				return Default;
			}
			return powerWeights;
		}
	}
	internal static class PowerRating
	{
		public const string DefaultFormat = "{name} — {deaths}   {P} ({C},{A},{H})";

		public const string DefaultLabels = "Combat,Agility,Trades";

		public static float Compute(SkillProfile profile, PowerWeights weights)
		{
			if (!profile.HasData)
			{
				return 0f;
			}
			PowerWeights powerWeights = weights ?? PowerWeights.Default;
			return powerWeights.Combat * profile.Combat + powerWeights.Acro * profile.Acro + powerWeights.Hand * profile.Hand;
		}

		public static int Display(float value)
		{
			return (int)Math.Round(value, MidpointRounding.AwayFromZero);
		}

		private static string Number(float value)
		{
			return Display(value).ToString(CultureInfo.InvariantCulture);
		}

		public static bool HasVisibleRating(SkillProfile profile, PowerWeights weights)
		{
			if (!profile.HasData)
			{
				return false;
			}
			if (Display(Compute(profile, weights)) == 0 && Display(profile.Combat) == 0 && Display(profile.Acro) == 0)
			{
				return Display(profile.Hand) != 0;
			}
			return true;
		}

		public static string StatsBlock(SkillProfile profile, PowerWeights weights)
		{
			if (!HasVisibleRating(profile, weights))
			{
				return "";
			}
			return Number(Compute(profile, weights)) + " (" + Number(profile.Combat) + "," + Number(profile.Acro) + "," + Number(profile.Hand) + ")";
		}

		public static string Line(string format, string name, int deaths, SkillProfile profile, PowerWeights weights, bool showPower)
		{
			string text = (string.IsNullOrEmpty(format) ? "{name} — {deaths}   {P} ({C},{A},{H})" : format);
			if (!showPower || !HasVisibleRating(profile, weights))
			{
				text = TrimAtPower(text);
			}
			return text.Replace("{name}", name ?? "?").Replace("{deaths}", deaths.ToString(CultureInfo.InvariantCulture)).Replace("{P}", Number(Compute(profile, weights)))
				.Replace("{C}", Number(profile.Combat))
				.Replace("{A}", Number(profile.Acro))
				.Replace("{H}", Number(profile.Hand));
		}

		public static string LegendLine(string format, string[] labels, bool showPower)
		{
			string text = (string.IsNullOrEmpty(format) ? "{name} — {deaths}   {P} ({C},{A},{H})" : format);
			if (!showPower)
			{
				text = TrimAtPower(text);
			}
			string[] array = ((labels != null && labels.Length == 3) ? labels : ParseLabels("Combat,Agility,Trades"));
			return text.Replace("{name}", "NAME").Replace("{deaths}", "DEATHS").Replace("{P}", "P")
				.Replace("{C}", array[0])
				.Replace("{A}", array[1])
				.Replace("{H}", array[2]);
		}

		public static string FormulaText(PowerWeights weights, string[] labels)
		{
			PowerWeights powerWeights = weights ?? PowerWeights.Default;
			string[] array = ((labels != null && labels.Length == 3) ? labels : ParseLabels("Combat,Agility,Trades"));
			return "Power Level = " + Percent(powerWeights.Combat) + " " + array[0] + " + " + Percent(powerWeights.Acro) + " " + array[1] + " + " + Percent(powerWeights.Hand) + " " + array[2];
		}

		private static string Percent(float weight)
		{
			float num = weight * 100f;
			return ((Math.Abs(num - (float)Math.Round(num)) < 0.05f) ? ((int)Math.Round(num)).ToString(CultureInfo.InvariantCulture) : num.ToString("0.#", CultureInfo.InvariantCulture)) + "%";
		}

		public static string[] ParseLabels(string value)
		{
			string[] array = (value ?? "").Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length != 3)
			{
				array = "Combat,Agility,Trades".Split(new char[1] { ',' });
			}
			string[] array2 = new string[3];
			for (int i = 0; i < 3; i++)
			{
				array2[i] = array[i].Trim();
			}
			return array2;
		}

		private static string TrimAtPower(string format)
		{
			int num = format.IndexOf("{P}", StringComparison.Ordinal);
			if (num >= 0)
			{
				return format.Substring(0, num).TrimEnd(Array.Empty<char>());
			}
			return format;
		}

		public static float BucketSum(IList<int> bucket, IDictionary<int, float> levels)
		{
			if (bucket == null || bucket.Count == 0)
			{
				return 0f;
			}
			float num = 0f;
			for (int i = 0; i < bucket.Count; i++)
			{
				if (levels != null && levels.TryGetValue(bucket[i], out var value))
				{
					num += value;
				}
			}
			return num;
		}

		public static void AppendLine(StringBuilder sb, string line)
		{
			if (sb.Length > 0)
			{
				sb.Append('\n');
			}
			sb.Append(line);
		}
	}
	internal struct ProfileCause
	{
		public string Key;

		public string Display;

		public int Count;
	}
	internal struct ProfileDeath
	{
		public string Utc;

		public int Day;

		public string Key;

		public string Display;
	}
	internal struct ProfileStat
	{
		public string Key;

		public string Label;

		public byte Group;

		public string Value;

		public float Raw;
	}
	internal struct ProfileSkill
	{
		public int Id;

		public string Label;

		public byte Group;

		public float Level;
	}
	internal struct ProfileEquip
	{
		public string Prefab;

		public byte Slot;

		public byte Quality;
	}
	internal struct ProfileRequest
	{
		public int Protocol;

		public string Name;
	}
	internal sealed class ProfileView
	{
		public int Protocol;

		public string Name = "";

		public bool Found;

		public bool Online;

		public int Deaths;

		public int Attributed;

		public bool HasSkills;

		public float Combat;

		public float Acro;

		public float Hand;

		public float Power;

		public string[] Labels = new string[3] { "Combat", "Agility", "Trades" };

		public string FirstSeenUtc = "";

		public string LastSeenUtc = "";

		public List<ProfileCause> Causes = new List<ProfileCause>();

		public List<ProfileDeath> Recent = new List<ProfileDeath>();

		public List<ProfileStat> Stats = new List<ProfileStat>();

		public List<ProfileCause> Kills = new List<ProfileCause>();

		public List<ProfileSkill> Skills = new List<ProfileSkill>();

		public List<ProfileEquip> Equipment = new List<ProfileEquip>();
	}
	internal static class ProfilePayload
	{
		public const int MaxCauses = 256;

		public const int MaxRecent = 64;

		public const int MaxStats = 128;

		public const int MaxKills = 256;

		public const int MaxSkills = 64;

		public const int MaxEquipment = 24;

		public static ZPackage WriteRequest(string name)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(BoardPayload.Trim(name));
			return val;
		}

		public static bool TryReadRequest(ZPackage pkg, out ProfileRequest request)
		{
			request = default(ProfileRequest);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				request.Protocol = pkg.ReadInt();
				request.Name = BoardPayload.Trim(pkg.ReadString());
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteProfile(ProfileView view)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(BoardPayload.Trim(view.Name));
			val.Write(view.Found);
			val.Write(view.Online);
			val.Write(view.Deaths);
			val.Write(view.Attributed);
			val.Write(view.HasSkills);
			val.Write(view.Combat);
			val.Write(view.Acro);
			val.Write(view.Hand);
			val.Write(view.Power);
			string[] array = ((view.Labels != null && view.Labels.Length == 3) ? view.Labels : new string[3] { "Combat", "Agility", "Trades" });
			val.Write(array[0] ?? "");
			val.Write(array[1] ?? "");
			val.Write(array[2] ?? "");
			val.Write(view.FirstSeenUtc ?? "");
			val.Write(view.LastSeenUtc ?? "");
			int num = ((view.Causes != null) ? Math.Min(view.Causes.Count, 256) : 0);
			val.Write(num);
			for (int i = 0; i < num; i++)
			{
				ProfileCause profileCause = view.Causes[i];
				val.Write(DeathCause.Trim(profileCause.Key));
				val.Write(DeathCause.Trim(profileCause.Display));
				val.Write(profileCause.Count);
			}
			int num2 = ((view.Recent != null) ? Math.Min(view.Recent.Count, 64) : 0);
			val.Write(num2);
			for (int j = 0; j < num2; j++)
			{
				ProfileDeath profileDeath = view.Recent[j];
				val.Write(profileDeath.Utc ?? "");
				val.Write(profileDeath.Day);
				val.Write(DeathCause.Trim(profileDeath.Key));
				val.Write(DeathCause.Trim(profileDeath.Display));
			}
			int num3 = ((view.Stats != null) ? Math.Min(view.Stats.Count, 128) : 0);
			val.Write(num3);
			for (int k = 0; k < num3; k++)
			{
				ProfileStat profileStat = view.Stats[k];
				val.Write(DeathCause.Trim(profileStat.Key));
				val.Write(BoardPayload.Trim(profileStat.Label));
				val.Write(profileStat.Group);
				val.Write(BoardPayload.Trim(profileStat.Value));
				val.Write(profileStat.Raw);
			}
			int num4 = ((view.Kills != null) ? Math.Min(view.Kills.Count, 256) : 0);
			val.Write(num4);
			for (int l = 0; l < num4; l++)
			{
				ProfileCause profileCause2 = view.Kills[l];
				val.Write(DeathCause.Trim(profileCause2.Key));
				val.Write(DeathCause.Trim(profileCause2.Display));
				val.Write(profileCause2.Count);
			}
			int num5 = ((view.Skills != null) ? Math.Min(view.Skills.Count, 64) : 0);
			val.Write(num5);
			for (int m = 0; m < num5; m++)
			{
				ProfileSkill profileSkill = view.Skills[m];
				val.Write(profileSkill.Id);
				val.Write(BoardPayload.Trim(profileSkill.Label));
				val.Write(profileSkill.Group);
				val.Write(profileSkill.Level);
			}
			int num6 = ((view.Equipment != null) ? Math.Min(view.Equipment.Count, 24) : 0);
			val.Write(num6);
			for (int n = 0; n < num6; n++)
			{
				ProfileEquip profileEquip = view.Equipment[n];
				val.Write(DeathCause.Trim(profileEquip.Prefab));
				val.Write(profileEquip.Slot);
				val.Write(profileEquip.Quality);
			}
			return val;
		}

		public static bool TryReadProfile(ZPackage pkg, out ProfileView view)
		{
			view = null;
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				ProfileView profileView = new ProfileView();
				profileView.Protocol = pkg.ReadInt();
				profileView.Name = BoardPayload.Trim(pkg.ReadString());
				profileView.Found = pkg.ReadBool();
				profileView.Online = pkg.ReadBool();
				profileView.Deaths = pkg.ReadInt();
				profileView.Attributed = pkg.ReadInt();
				profileView.HasSkills = pkg.ReadBool();
				profileView.Combat = pkg.ReadSingle();
				profileView.Acro = pkg.ReadSingle();
				profileView.Hand = pkg.ReadSingle();
				profileView.Power = pkg.ReadSingle();
				profileView.Labels = new string[3]
				{
					pkg.ReadString() ?? "Combat",
					pkg.ReadString() ?? "Agility",
					pkg.ReadString() ?? "Trades"
				};
				profileView.FirstSeenUtc = pkg.ReadString() ?? "";
				profileView.LastSeenUtc = pkg.ReadString() ?? "";
				int num = pkg.ReadInt();
				if (num < 0 || num > 256)
				{
					return false;
				}
				profileView.Causes = new List<ProfileCause>(num);
				for (int i = 0; i < num; i++)
				{
					ProfileCause item = new ProfileCause
					{
						Key = DeathCause.Trim(pkg.ReadString()),
						Display = DeathCause.Trim(pkg.ReadString()),
						Count = pkg.ReadInt()
					};
					if (item.Count > 0 && !string.IsNullOrEmpty(item.Key))
					{
						profileView.Causes.Add(item);
					}
				}
				int num2 = pkg.ReadInt();
				if (num2 < 0 || num2 > 64)
				{
					return false;
				}
				profileView.Recent = new List<ProfileDeath>(num2);
				for (int j = 0; j < num2; j++)
				{
					ProfileDeath item2 = new ProfileDeath
					{
						Utc = (pkg.ReadString() ?? ""),
						Day = pkg.ReadInt(),
						Key = DeathCause.Trim(pkg.ReadString()),
						Display = DeathCause.Trim(pkg.ReadString())
					};
					if (!string.IsNullOrEmpty(item2.Key))
					{
						profileView.Recent.Add(item2);
					}
				}
				int num3 = pkg.ReadInt();
				if (num3 < 0 || num3 > 128)
				{
					return false;
				}
				profileView.Stats = new List<ProfileStat>(num3);
				for (int k = 0; k < num3; k++)
				{
					ProfileStat item3 = new ProfileStat
					{
						Key = DeathCause.Trim(pkg.ReadString()),
						Label = BoardPayload.Trim(pkg.ReadString()),
						Group = pkg.ReadByte(),
						Value = BoardPayload.Trim(pkg.ReadString()),
						Raw = pkg.ReadSingle()
					};
					if (!string.IsNullOrEmpty(item3.Key))
					{
						profileView.Stats.Add(item3);
					}
				}
				int num4 = pkg.ReadInt();
				if (num4 < 0 || num4 > 256)
				{
					return false;
				}
				profileView.Kills = new List<ProfileCause>(num4);
				for (int l = 0; l < num4; l++)
				{
					ProfileCause item4 = new ProfileCause
					{
						Key = DeathCause.Trim(pkg.ReadString()),
						Display = DeathCause.Trim(pkg.ReadString()),
						Count = pkg.ReadInt()
					};
					if (item4.Count > 0 && !string.IsNullOrEmpty(item4.Key))
					{
						profileView.Kills.Add(item4);
					}
				}
				int num5 = pkg.ReadInt();
				if (num5 < 0 || num5 > 64)
				{
					return false;
				}
				profileView.Skills = new List<ProfileSkill>(num5);
				for (int m = 0; m < num5; m++)
				{
					ProfileSkill item5 = new ProfileSkill
					{
						Id = pkg.ReadInt(),
						Label = BoardPayload.Trim(pkg.ReadString()),
						Group = pkg.ReadByte(),
						Level = pkg.ReadSingle()
					};
					if (item5.Level > 0f)
					{
						profileView.Skills.Add(item5);
					}
				}
				int num6 = pkg.ReadInt();
				if (num6 < 0 || num6 > 24)
				{
					return false;
				}
				profileView.Equipment = new List<ProfileEquip>(num6);
				for (int n = 0; n < num6; n++)
				{
					ProfileEquip item6 = new ProfileEquip
					{
						Prefab = DeathCause.Trim(pkg.ReadString()),
						Slot = pkg.ReadByte(),
						Quality = pkg.ReadByte()
					};
					if (!string.IsNullOrEmpty(item6.Prefab))
					{
						profileView.Equipment.Add(item6);
					}
				}
				view = profileView;
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}
	}
	internal static class RealmProtocol
	{
		internal struct Hello
		{
			public int Protocol;

			public int ClientBuild;

			public string SteamId;

			public string CharacterName;
		}

		internal struct HelloAck
		{
			public int Protocol;

			public int ServerBuild;

			public bool RealmMode;

			public bool KickUnmodded;

			public bool RemoteBoard;

			public bool AcceptsSkills;

			public bool AcceptsCharUploads;

			public bool AcceptsDeathReports;

			public bool AcceptsStats;

			public string ServerLabel;
		}

		internal struct SkillsReport
		{
			public int Protocol;

			public string SteamId;

			public string CharacterName;

			public Dictionary<int, float> Levels;
		}

		internal struct DeathReport
		{
			public int Protocol;

			public string SteamId;

			public string CharacterName;

			public byte HitType;

			public string AttackerKey;

			public string AttackerDisplay;

			public bool AttackerIsPlayer;
		}

		internal struct StatsReport
		{
			public int Protocol;

			public string SteamId;

			public string CharacterName;

			public int SessionNonce;

			public Dictionary<int, float> Counters;

			public Dictionary<string, float> Mod;

			public Dictionary<string, float> Enemies;

			public List<EquippedItem> Equipment;
		}

		internal struct EquippedItem
		{
			public string Prefab;

			public byte Slot;

			public byte Quality;
		}

		internal enum UploadReason : byte
		{
			Interval,
			Death,
			Logout,
			Inventory
		}

		internal struct CharUpload
		{
			public int Protocol;

			public string SteamId;

			public string CharacterName;

			public int Generation;

			public UploadReason Reason;

			public byte[] Blob;

			public byte[] World;
		}

		internal struct CharOffer
		{
			public int Protocol;

			public string CharacterName;
		}

		internal struct CharPush
		{
			public int Protocol;

			public int Generation;

			public byte[] Blob;

			public byte[] World;
		}

		internal struct CharApplied
		{
			public int Protocol;

			public int Generation;

			public bool Ok;

			public string Note;
		}

		public const byte PacketVersion = 9;

		public const int ProtocolVersion = 9;

		public const string RpcHello = "Deathboard_Hello";

		public const string RpcHelloAck = "Deathboard_HelloAck";

		public const string RpcSkills = "Deathboard_Skills";

		public const string RpcCharOffer = "Deathboard_CharOffer";

		public const string RpcCharPush = "Deathboard_CharPush";

		public const string RpcCharApplied = "Deathboard_CharApplied";

		public const string RpcCharUpload = "Deathboard_CharUpload";

		public const string RpcBoardReq = "Deathboard_BoardReq";

		public const string RpcBoard = "Deathboard_Board";

		public const string RpcDeathReport = "Deathboard_DeathReport";

		public const string RpcProfileReq = "Deathboard_ProfileReq";

		public const string RpcProfile = "Deathboard_Profile";

		public const string RpcStats = "Deathboard_Stats";

		public const int MaxReportedSkills = 128;

		public const int MaxEquipment = 24;

		public const int MaxSnapshotBytes = 4194304;

		public static ZPackage WriteHello(string steamId, string characterName)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: 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_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(51);
			val.Write(steamId ?? "");
			val.Write(characterName ?? "");
			return val;
		}

		public static bool TryReadHello(ZPackage pkg, out Hello hello)
		{
			hello = default(Hello);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				hello.Protocol = pkg.ReadInt();
				hello.ClientBuild = pkg.ReadInt();
				hello.SteamId = pkg.ReadString();
				hello.CharacterName = pkg.ReadString();
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteHelloAck(bool realmMode, bool kickUnmodded, bool remoteBoard, bool acceptsSkills, bool acceptsCharUploads, bool acceptsDeathReports, bool acceptsStats, string serverLabel)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: 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_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: 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)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(51);
			val.Write(realmMode);
			val.Write(kickUnmodded);
			val.Write(remoteBoard);
			val.Write(acceptsSkills);
			val.Write(acceptsCharUploads);
			val.Write(acceptsDeathReports);
			val.Write(acceptsStats);
			val.Write(serverLabel ?? "");
			return val;
		}

		public static bool TryReadHelloAck(ZPackage pkg, out HelloAck ack)
		{
			ack = default(HelloAck);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				ack.Protocol = pkg.ReadInt();
				ack.ServerBuild = pkg.ReadInt();
				ack.RealmMode = pkg.ReadBool();
				ack.KickUnmodded = pkg.ReadBool();
				ack.RemoteBoard = pkg.ReadBool();
				ack.AcceptsSkills = pkg.ReadBool();
				ack.AcceptsCharUploads = pkg.ReadBool();
				ack.AcceptsDeathReports = pkg.ReadBool();
				ack.AcceptsStats = pkg.ReadBool();
				ack.ServerLabel = pkg.ReadString();
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteSkills(string steamId, string characterName, Dictionary<int, float> levels)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(steamId ?? "");
			val.Write(characterName ?? "");
			int num = ((levels != null) ? Math.Min(levels.Count, 128) : 0);
			val.Write(num);
			if (levels != null)
			{
				int num2 = 0;
				foreach (KeyValuePair<int, float> level in levels)
				{
					if (num2 >= num)
					{
						break;
					}
					val.Write(level.Key);
					val.Write(level.Value);
					num2++;
				}
			}
			return val;
		}

		public static bool TryReadSkills(ZPackage pkg, out SkillsReport report)
		{
			report = default(SkillsReport);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				report.Protocol = pkg.ReadInt();
				report.SteamId = pkg.ReadString();
				report.CharacterName = pkg.ReadString();
				int num = pkg.ReadInt();
				if (num < 0 || num > 128)
				{
					return false;
				}
				Dictionary<int, float> dictionary = new Dictionary<int, float>(num);
				for (int i = 0; i < num; i++)
				{
					int key = pkg.ReadInt();
					float num2 = pkg.ReadSingle();
					if (float.IsNaN(num2) || num2 < 0f)
					{
						num2 = 0f;
					}
					if (num2 > 100f)
					{
						num2 = 100f;
					}
					dictionary[key] = num2;
				}
				report.Levels = dictionary;
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteDeathReport(string steamId, string characterName, byte hitType, string attackerKey, string attackerDisplay, bool attackerIsPlayer)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: 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_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(steamId ?? "");
			val.Write(characterName ?? "");
			val.Write(hitType);
			val.Write(DeathCause.Trim(attackerKey));
			val.Write(DeathCause.Trim(attackerDisplay));
			val.Write(attackerIsPlayer);
			return val;
		}

		public static bool TryReadDeathReport(ZPackage pkg, out DeathReport report)
		{
			report = default(DeathReport);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				report.Protocol = pkg.ReadInt();
				report.SteamId = pkg.ReadString();
				report.CharacterName = pkg.ReadString();
				byte b = pkg.ReadByte();
				report.HitType = (byte)((b < DeathCause.HitTypeCount) ? b : 0);
				report.AttackerKey = DeathCause.Trim(pkg.ReadString());
				report.AttackerDisplay = DeathCause.Trim(pkg.ReadString());
				report.AttackerIsPlayer = pkg.ReadBool();
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteStats(string steamId, string characterName, int sessionNonce, Dictionary<int, float> counters, Dictionary<string, float> mod, Dictionary<string, float> enemies, List<EquippedItem> equipment)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(steamId ?? "");
			val.Write(characterName ?? "");
			val.Write(sessionNonce);
			WriteIntMap(val, counters, 256);
			WriteStringMap(val, mod, 64);
			WriteStringMap(val, enemies, 512);
			int num = ((equipment != null) ? Math.Min(equipment.Count, 24) : 0);
			val.Write(num);
			for (int i = 0; i < num; i++)
			{
				EquippedItem equippedItem = equipment[i];
				val.Write(DeathCause.Trim(equippedItem.Prefab));
				val.Write(equippedItem.Slot);
				val.Write(equippedItem.Quality);
			}
			return val;
		}

		private static void WriteIntMap(ZPackage pkg, Dictionary<int, float> map, int cap)
		{
			int num = ((map != null) ? Math.Min(map.Count, cap) : 0);
			pkg.Write(num);
			if (map == null)
			{
				return;
			}
			int num2 = 0;
			foreach (KeyValuePair<int, float> item in map)
			{
				if (num2 >= num)
				{
					break;
				}
				pkg.Write(item.Key);
				pkg.Write(item.Value);
				num2++;
			}
		}

		private static void WriteStringMap(ZPackage pkg, Dictionary<string, float> map, int cap)
		{
			int num = ((map != null) ? Math.Min(map.Count, cap) : 0);
			pkg.Write(num);
			if (map == null)
			{
				return;
			}
			int num2 = 0;
			foreach (KeyValuePair<string, float> item in map)
			{
				if (num2 >= num)
				{
					break;
				}
				pkg.Write(DeathCause.Trim(item.Key));
				pkg.Write(item.Value);
				num2++;
			}
		}

		public static bool TryReadStats(ZPackage pkg, out StatsReport report)
		{
			report = default(StatsReport);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				report.Protocol = pkg.ReadInt();
				report.SteamId = pkg.ReadString();
				report.CharacterName = pkg.ReadString();
				report.SessionNonce = pkg.ReadInt();
				int num = pkg.ReadInt();
				if (num < 0 || num > 256)
				{
					return false;
				}
				report.Counters = new Dictionary<int, float>(num);
				for (int i = 0; i < num; i++)
				{
					int key = pkg.ReadInt();
					float value = pkg.ReadSingle();
					if (Sane(value))
					{
						report.Counters[key] = value;
					}
				}
				if (!TryReadStringMap(pkg, 64, out report.Mod))
				{
					return false;
				}
				if (!TryReadStringMap(pkg, 512, out report.Enemies))
				{
					return false;
				}
				int num2 = pkg.ReadInt();
				if (num2 < 0 || num2 > 24)
				{
					return false;
				}
				report.Equipment = new List<EquippedItem>(num2);
				for (int j = 0; j < num2; j++)
				{
					EquippedItem item = new EquippedItem
					{
						Prefab = DeathCause.Trim(pkg.ReadString()),
						Slot = pkg.ReadByte(),
						Quality = pkg.ReadByte()
					};
					if (!string.IsNullOrEmpty(item.Prefab))
					{
						report.Equipment.Add(item);
					}
				}
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		private static bool TryReadStringMap(ZPackage pkg, int cap, out Dictionary<string, float> map)
		{
			map = null;
			int num = pkg.ReadInt();
			if (num < 0 || num > cap)
			{
				return false;
			}
			map = new Dictionary<string, float>(num, StringComparer.Ordinal);
			for (int i = 0; i < num; i++)
			{
				string text = DeathCause.Trim(pkg.ReadString());
				float value = pkg.ReadSingle();
				if (!string.IsNullOrEmpty(text) && Sane(value))
				{
					map[text] = value;
				}
			}
			return true;
		}

		private static bool Sane(float value)
		{
			if (!float.IsNaN(value) && !float.IsInfinity(value))
			{
				return value >= 0f;
			}
			return false;
		}

		public static ZPackage WriteCharUpload(string steamId, string characterName, int generation, UploadReason reason, byte[] blob, byte[] world)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: 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_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(steamId ?? "");
			val.Write(characterName ?? "");
			val.Write(generation);
			val.Write((byte)reason);
			val.Write(blob ?? new byte[0]);
			val.Write(world ?? new byte[0]);
			return val;
		}

		public static bool TryReadCharUpload(ZPackage pkg, out CharUpload upload)
		{
			upload = default(CharUpload);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				upload.Protocol = pkg.ReadInt();
				upload.SteamId = pkg.ReadString();
				upload.CharacterName = pkg.ReadString();
				upload.Generation = pkg.ReadInt();
				byte b = pkg.ReadByte();
				upload.Reason = (UploadReason)((b <= 3) ? b : 0);
				byte[] array = pkg.ReadByteArray();
				if (array == null || array.Length == 0)
				{
					return false;
				}
				if (array.Length > 4194304)
				{
					return false;
				}
				upload.Blob = array;
				byte[] array2 = pkg.ReadByteArray();
				upload.World = ((array2 != null && array2.Length <= 1048576) ? array2 : null);
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteCharOffer(string characterName)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(characterName ?? "");
			return val;
		}

		public static bool TryReadCharOffer(ZPackage pkg, out CharOffer offer)
		{
			offer = default(CharOffer);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				offer.Protocol = pkg.ReadInt();
				offer.CharacterName = pkg.ReadString();
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteCharPush(int generation, byte[] blob, byte[] world)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(generation);
			val.Write(blob ?? new byte[0]);
			val.Write(world ?? new byte[0]);
			return val;
		}

		public static bool TryReadCharPush(ZPackage pkg, out CharPush push)
		{
			push = default(CharPush);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				push.Protocol = pkg.ReadInt();
				push.Generation = pkg.ReadInt();
				byte[] array = pkg.ReadByteArray();
				if (array == null || array.Length == 0)
				{
					return false;
				}
				if (array.Length > 4194304)
				{
					return false;
				}
				push.Blob = array;
				byte[] array2 = pkg.ReadByteArray();
				push.World = ((array2 != null && array2.Length <= 1048576) ? array2 : null);
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static ZPackage WriteCharApplied(int generation, bool ok, string note)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)9);
			val.Write(9);
			val.Write(generation);
			val.Write(ok);
			val.Write(note ?? "");
			return val;
		}

		public static bool TryReadCharApplied(ZPackage pkg, out CharApplied applied)
		{
			applied = default(CharApplied);
			if (pkg == null)
			{
				return false;
			}
			try
			{
				pkg.SetPos(0);
				if (pkg.ReadByte() != 9)
				{
					return false;
				}
				applied.Protocol = pkg.ReadInt();
				applied.Generation = pkg.ReadInt();
				applied.Ok = pkg.ReadBool();
				applied.Note = pkg.ReadString();
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}
	}
	internal static class RealmWorldData
	{
		private const byte Version = 1;

		public const int MaxBytes = 1048576;

		public static byte[] Capture(PlayerProfile profile)
		{
			//IL_0029: 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_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Expected O, but got Unknown
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: 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_0098: Expected O, but got Unknown
			//IL_0098: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			if (profile == null)
			{
				return null;
			}
			try
			{
				profile.SaveLogoutPoint();
				if ((Object)(object)Minimap.instance != (Object)null)
				{
					Minimap.instance.SaveMapData();
				}
			}
			catch (Exception)
			{
			}
			try
			{
				ZPackage val = new ZPackage();
				val.Write((byte)1);
				WritePoint(val, profile.HaveLogoutPoint(), profile.HaveLogoutPoint() ? profile.GetLogoutPoint() : Vector3.zero);
				WritePoint(val, profile.HaveDeathPoint(), profile.HaveDeathPoint() ? profile.GetDeathPoint() : Vector3.zero);
				WritePoint(val, profile.HaveCustomSpawnPoint(), profile.HaveCustomSpawnPoint() ? profile.GetCustomSpawnPoint() : Vector3.zero);
				val.Write(profile.GetHomePoint());
				byte[] mapData = profile.GetMapData();
				val.Write(mapData ?? new byte[0]);
				byte[] array = val.GetArray();
				return (array != null && array.Length <= 1048576) ? array : null;
			}
			catch (Exception)
			{
				return null;
			}
		}

		public static bool Apply(PlayerProfile profile, byte[] blob, out byte[] mapData)
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Expected O, but got Unknown
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			mapData = null;
			if (profile == null || blob == null || blob.Length == 0)
			{
				return false;
			}
			try
			{
				ZPackage val = new ZPackage(blob);
				if (val.ReadByte() != 1)
				{
					return false;
				}
				if (ReadPoint(val, out var point))
				{
					profile.SetLogoutPoint(point);
				}
				else
				{
					profile.ClearLoguoutPoint();
				}
				if (ReadPoint(val, out var point2))
				{
					profile.SetDeathPoint(point2);
				}
				if (ReadPoint(val, out var point3))
				{
					profile.SetCustomSpawnPoint(point3);
				}
				else
				{
					profile.ClearCustomSpawnPoint();
				}
				profile.SetHomePoint(val.ReadVector3());
				byte[] array = val.ReadByteArray();
				if (array != null && array.Length != 0)
				{
					profile.SetMapData(array);
					mapData = array;
				}
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}

		private static void WritePoint(ZPackage pkg, bool have, Vector3 point)
		{
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			pkg.Write(have);
			pkg.Write(point);
		}

		private static bool ReadPoint(ZPackage pkg, out Vector3 point)
		{
			//IL_0008: 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)
			bool result = pkg.ReadBool();
			point = pkg.ReadVector3();
			return result;
		}
	}
	internal static class SkillBuckets
	{
		public static readonly SkillType[] Combat;

		public static readonly SkillType[] Athletics;

		public static readonly SkillType[] Hand;

		public static readonly int[] CombatIds;

		public static readonly int[] AthleticsIds;

		public static readonly int[] HandIds;

		private static int[] ToIds(SkillType[] types)
		{
			int[] array = new int[types.Length];
			for (int i = 0; i < types.Length; i++)
			{
				array[i] = (int)types[i];
			}
			return array;
		}

		public static SkillProfile Profile(IDictionary<int, float> levels)
		{
			if (levels == null)
			{
				return SkillProfile.Unknown;
			}
			return new SkillProfile
			{
				HasData = true,
				Combat = PowerRating.BucketSum(CombatIds, levels),
				Acro = PowerRating.BucketSum(AthleticsIds, levels),
				Hand = PowerRating.BucketSum(HandIds, levels)
			};
		}

		public static bool IsRealSkillId(int id)
		{
			if (id == 0 || id == 999)
			{
				return false;
			}
			return Enum.IsDefined(typeof(SkillType), id);
		}

		static SkillBuckets()
		{
			SkillType[] array = new SkillType[13];
			RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/);
			Combat = (SkillType[])(object)array;
			SkillType[] array2 = new SkillType[5];
			RuntimeHelpers.InitializeArray(array2, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/);
			Athletics = (SkillType[])(object)array2;
			SkillType[] array3 = new SkillType[6];
			RuntimeHelpers.InitializeArray(array3, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/);
			Hand = (SkillType[])(object)array3;
			CombatIds = ToIds(Combat);
			AthleticsIds = ToIds(Athletics);
			HandIds = ToIds(Hand);
		}
	}
	internal enum StatGroup
	{
		Combat,
		Craft,
		Land,
		Travel,
		Death
	}
	internal sealed class StatDef
	{
		public string Key = "";

		public int Ordinal = -1;

		public string Label = "";

		public StatGroup Group;

		public bool IsDistance;
	}
	internal static class StatCatalog
	{
		public const string ModBlocks = "blocks";

		public const string ModParries = "parries";

		public const int MaxCounters = 256;

		public const int MaxModCounters = 64;

		public const int MaxEnemyRows = 512;

		private static readonly List<StatDef> All = new List<StatDef>
		{
			new StatDef
			{
				Key = "kills",
				Ordinal = 6,
				Label = "Monsters Slain",
				Group = StatGroup.Combat
			},
			new StatDef
			{
				Key = "bosskills",
				Ordinal = 85,
				Label = "Bosses Felled",
				Group = StatGroup.Combat
			},
			new StatDef
			{
				Key = "blocks",
				Ordinal = -1,
				Label = "Blocks",
				Group = StatGroup.Combat
			},
			new StatDef
			{
				Key = "parries",
				Ordinal = -1,
				Label = "Parries",
				Group = StatGroup.Combat
			},
			new StatDef
			{
				Key = "crafts",
				Ordinal = 13,
				Label = "Items Crafted",
				Group = StatGroup.Craft
			},
			new StatDef
			{
				Key = "builds",
				Ordinal = 2,
				Label = "Pieces Built",
				Group = StatGroup.Craft
			},
			new StatDef
			{
				Key = "trees",
				Ordinal = 28,
				Label = "Trees Felled",
				Group = StatGroup.Land
			},
			new StatDef
			{
				Key = "mines",
				Ordinal = 38,
				Label = "Ore Mined",
				Group = StatGroup.Land
			},
			new StatDef
			{
				Key = "dist",
				Ordinal = 16,
				Label = "Distance Travelled",
				Group = StatGroup.Travel,
				IsDistance = true
			},
			new StatDef
			{
				Key = "sail",
				Ordinal = 19,
				Label = "Distance Sailed",
				Group = StatGroup.Travel,
				IsDistance = true
			}
		};

		private static readonly string[] DeathByLabels = new string[25]
		{
			"Unknown", "Enemies", "Vikings", "Falling", "Drowning", "Fire", "Cold", "Poison", "Smoke", "Deep Water",
			"Edge of the World", "Impact", "Carts", "Falling Trees", "Own Hand", "Structures", "Turrets", "Boats", "Stalagmites", "Catapults",
			"Cinder Fire", "The Ashlands Ocean", "Incinerators", "Drawbridges", "Ashlands Lava"
		};

		private static readonly int[] DeathByOrdinals = new int[25]
		{
			55, 56, 57, 58, 59, 60, 61, 62, 63, 64,
			65, 66, 67, 68, 69, 70, 71, 72, 73, 105,
			106, 107, 108, 203, 204
		};

		private static readonly Dictionary<string, StatDef> ByKey = BuildIndex();

		private static readonly Dictionary<int, StatDef> ByOrdinal = BuildOrdinalIndex();

		public const int GroupCount = 5;

		private static Dictionary<string, StatDef> BuildIndex()
		{
			Dictionary<string, StatDef> dictionary = new Dictionary<string, StatDef>(StringComparer.Ordinal);
			foreach (StatDef item in Definitions())
			{
				dictionary[item.Key] = item;
			}
			return dictionary;
		}

		private static Dictionary<int, StatDef> BuildOrdinalIndex()
		{
			Dictionary<int, StatDef> dictionary = new Dictionary<int, StatDef>();
			foreach (StatDef item in Definitions())
			{
				if (item.Ordinal >= 0)
				{
					dictionary[item.Ordinal] = item;
				}
			}
			return dictionary;
		}

		public static IEnumerable<StatDef> Definitions()
		{
			for (int i = 0; i < All.Count; i++)
			{
				yield return All[i];
			}
			for (int i = 0; i < DeathByLabels.Length; i++)
			{
				yield return new StatDef
				{
					Key = "death." + i,
					Ordinal = DeathByOrdinals[i],
					Label = DeathByLabels[i],
					Group = StatGroup.Death
				};
			}
		}

		public static List<StatDef> InGroup(StatGroup group)
		{
			List<StatDef> list = new List<StatDef>();
			foreach (StatDef item in Definitions())
			{
				if (item.Group == group)
				{
					list.Add(item);
				}
			}
			return list;
		}

		public static bool TryByKey(string key, out StatDef def)
		{
			def = null;
			if (!string.IsNullOrEmpty(key))
			{
				return ByKey.TryGetValue(key, out def);
			}
			return false;
		}

		public static bool TryByOrdinal(int ordinal, out StatDef def)
		{
			return ByOrdinal.TryGetValue(ordinal, out def);
		}

		public static string GroupLabel(StatGroup group)
		{
			return group switch
			{
				StatGroup.Combat => "COMBAT", 
				StatGroup.Craft => "CRAFT", 
				StatGroup.Land => "LAND", 
				StatGroup.Travel => "TRAVEL", 
				StatGroup.Death => "CAUSE OF DEATH", 
				_ => "OTHER", 
			};
		}

		public static string Format(StatDef def, float value)
		{
			if (def != null && def.IsDistance)
			{
				if (value >= 1000f)
				{
					return (value / 1000f).ToString("0.0", CultureInfo.InvariantCulture) + " km";
				}
				return ((int)value).ToString(CultureInfo.InvariantCulture) + " m";
			}
			return Thousands((int)value);
		}

		public static string Thousands(int value)
		{
			return value.ToString("#,0", CultureInfo.InvariantCulture);
		}

		public static string PrettyToken(string token)
		{
			if (string.IsNullOrEmpty(token))
			{
				return "";
			}
			string text = token;
			if (text[0] == '$')
			{
				text = text.Substring(1);
			}
			string[] array = new string[4] { "enemy_", "npc_", "item_", "character_" };
			for (int i = 0; i < array.Length; i++)
			{
				if (text.StartsWith(array[i], StringComparison.OrdinalIgnoreCase))
				{
					text = text.Substring(array[i].Length);
					break;
				}
			}
			if (text.Length == 0)
			{
				return token;
			}
			StringBuilder stringBuilder = new StringBuilder(text.Length + 4);
			bool flag = true;
			foreach (char c in text)
			{
				if (c == '_' || c == ' ')
				{
					stringBuilder.Append(' ');
					flag = true;
				}
				else
				{
					stringBuilder.Append(flag ? char.ToUpperInvariant(c) : c);
					flag = false;
				}
			}
			return stringBuilder.ToString();
		}
	}
}
namespace Deathboard.Client
{
	internal static class BlockCounter
	{
		private const float PerfectBlockWindow = 0.25f;

		private const float NotBlocking = -1f;

		private static readonly FieldInfo BlockTimerField = AccessTools.Field(typeof(Humanoid), "m_blockTimer");

		public static float Blocks;

		public static float Parries;

		public static void Reset()
		{
			Blocks = 0f;
			Parries = 0f;
		}

		internal static void Note(Humanoid blocker)
		{
			if (BlockTimerField == null || (Object)(object)blocker == (Object)null || (Object)(object)blocker != (Object)(object)Player.m_localPlayer)
			{
				return;
			}
			float num;
			try
			{
				num = (float)BlockTimerField.GetValue(blocker);
			}
			catch (Exception)
			{
				return;
			}
			if (!(num <= -1f))
			{
				Blocks += 1f;
				if (num < 0.25f)
				{
					Parries += 1f;
				}
			}
		}
	}
	[HarmonyPatch]
	internal static class BlockCounterPatch
	{
		private static MethodBase TargetMethod()
		{
			return AccessTools.Method(typeof(Humanoid), "BlockAttack", new Type[2]
			{
				typeof(HitData),
				typeof(Character)
			}, (Type[])null);
		}

		private static void Prefix(Humanoid __instance)
		{
			try
			{
				BlockCounter.Note(__instance);
			}
			catch (Exception)
			{
			}
		}
	}
	internal static class BossIcons
	{
		private static readonly Dictionary<string, string[]> Candidates = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
		{
			{
				"defeated_eikthyr",
				new string[1] { "TrophyEikthyr" }
			},
			{
				"defeated_gdking",
				new string[3] { "TrophyTheElder", "TrophyGDKing", "TrophyElder" }
			},
			{
				"defeated_bonemass",
				new string[1] { "TrophyBonemass" }
			},
			{
				"defeated_dragon",
				new string[3] { "TrophyDragonQueen", "TrophyModer", "TrophyDragon" }
			},
			{
				"defeated_goblinking",
				new string[2] { "TrophyGoblinKing", "TrophyYagluth" }
			},
			{
				"defeated_queen",
				new string[2] { "TrophySeekerQueen", "TrophyQueen" }
			},
			{
				"defeated_fader",
				new string[1] { "TrophyFader" }
			}
		};

		private static readonly Dictionary<string, string> Stems = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
		{
			{ "defeated_eikthyr", "eikthyr" },
			{ "defeated_gdking", "elder" },
			{ "defeated_bonemass", "bonemass" },
			{ "defeated_dragon", "dragon" },
			{ "defeated_goblinking", "goblin" },
			{ "defeated_queen", "seeker" },
			{ "defeated_fader", "fader" }
		};

		private static readonly Dictionary<string, Sprite> Cache = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);

		private static ObjectDB _resolvedAgainst;

		private static readonly List<string> Resolved = new List<string>();

		private static readonly List<string> Missing = new List<string>();

		private static bool _reported;

		public static Sprite For(string globalKey)
		{
			if (string.IsNullOrEmpty(globalKey))
			{
				return null;
			}
			try
			{
				ObjectDB instance = ObjectDB.instance;
				if ((Object)(object)instance == (Object)null || instance.m_items == null)
				{
					return null;
				}
				if (instance != _resolvedAgainst)
				{
					_resolvedAgainst = instance;
					Cache.Clear();
					Resolved.Clear();
					Missing.Clear();
					_reported = false;
				}
				if (Cache.TryGetValue(globalKey, out var value))
				{
					return value;
				}
				Sprite val = Resolve(instance, globalKey);
				Cache[globalKey] = val;
				(((Object)(object)val != (Object)null) ? Resolved : Missing).Add(globalKey);
				return val;
			}
			catch (Exception)
			{
				return null;
			}
		}

		public static string ReportOnce()
		{
			if (_reported || (Resolved.Count == 0 && Missing.Count == 0))
			{
				return null;
			}
			_reported = true;
			return "Boss icons resolved " + Resolved.Count + "/" + (Resolved.Count + Missing.Count) + ((Missing.Count > 0) ? (". No trophy prefab found for: " + string.Join(", ", Missing.ToArray()) + " - those bosses draw without an icon. Add the right prefab name to BossIcons.Candidates.") : ".");
		}

		private static Sprite Resolve(ObjectDB db, string globalKey)
		{
			if (Candidates.TryGetValue(globalKey, out var value))
			{
				for (int i = 0; i < value.Length; i++)
				{
					Sprite val = IconOf(db.GetItemPrefab(value[i]));
					if ((Object)(object)val != (Object)null)
					{
						return val;
					}
				}
			}
			if (!Stems.TryGetValue(globalKey, out var value2))
			{
				return null;
			}
			for (int j = 0; j < db.m_items.Count; j++)
			{
				GameObject val2 = db.m_items[j];
				if ((Object)(object)val2 == (Object)null)
				{
					continue;
				}
				string name = ((Object)val2).name;
				if (!string.IsNullOrEmpty(name) && name.IndexOf("Trophy", StringComparison.OrdinalIgnoreCase) >= 0 && name.IndexOf(value2, StringComparison.OrdinalIgnoreCase) >= 0)
				{
					Sprite val3 = IconOf(val2);
					if ((Object)(object)val3 != (Object)null)
					{
						return val3;
					}
				}
			}
			return null;
		}

		private static Sprite IconOf(GameObject prefab)
		{
			if ((Object)(object)prefab == (Object)null)
			{
				return null;
			}
			ItemDrop component = prefab.GetComponent<ItemDrop>();
			if ((Object)(object)component == (Object)null || component.m_itemData == null || component.m_itemData.m_shared == null)
			{
				return null;
			}
			Sprite[] icons = component.m_itemData.m_shared.m_icons;
			if (icons == null || icons.Length == 0)
			{
				return null;
			}
			return icons[0];
		}

		public static void Draw(Rect rect, Sprite sprite, Color tint)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: 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_007b: 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_0088: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)sprite == (Object)null) && !((Object)(object)sprite.texture == (Object)null))
			{
				Rect textureRect = sprite.textureRect;
				Rect val = default(Rect);
				((Rect)(ref val))..ctor(((Rect)(ref textureRect)).x / (float)((Texture)sprite.texture).width, ((Rect)(ref textureRect)).y / (float)((Texture)sprite.texture).height, ((Rect)(ref textureRect)).width / (float)((Texture)sprite.texture).width, ((Rect)(ref textureRect)).height / (float)((Texture)sprite.texture).height);
				Color color = GUI.color;
				GUI.color = tint;
				GUI.DrawTextureWithTexCoords(rect, (Texture)(object)sprite.texture, val, true);
				GUI.color = color;
			}
		}
	}
	internal sealed class ClientConfig
	{
		public readonly ConfigEntry<bool> Enabled;

		public readonly ConfigEntry<float> HandshakeTimeoutSeconds;

		public readonly ConfigEntry<KeyCode> LadderKey;

		public readonly ConfigEntry<float> LadderRefreshSeconds;

		public readonly ConfigEntry<bool> LadderBlocksInput;

		public readonly ConfigEntry<float> LadderScale;

		public ClientConfig(ConfigFile config)
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			//IL_00a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Expected O, but got Unknown
			//IL_00fa: Unknown result type (might be due to invalid IL or missing references)
			//IL_0104: Expected O, but got Unknown
			Enabled = config.Bind<bool>("General", "Enabled", true, "Master switch. False makes this plugin do nothing at all, on every server.");
			HandshakeTimeoutSeconds = config.Bind<float>("General", "HandshakeTimeoutSeconds", 8f, new ConfigDescription("How long to wait for a realm server to answer before deciding this is an ordinary server and going inert for the session.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), Array.Empty<object>()));
			LadderKey = config.Bind<KeyCode>("Ladder", "Key", (KeyCode)288, "Key that opens and closes the leaderboard window. Vanilla Valheim binds F5 (console), F2, F9, F11, Ctrl+F1 and Ctrl+F3, and leaves the rest alone - but mods do claim F-keys, PlantEasily takes F6/F8/F10 for one. F7 is clear of all of those. Change it if something on your setup wants F7.");
			LadderRefreshSeconds = config.Bind<float>("Ladder", "RefreshSeconds", 5f, new ConfigDescription("How often the open window asks the server for a fresh board. The server rate-limits requests to one per second per player regardless of what is set here.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), Array.Empty<object>()));
			LadderBlocksInput = config.Bind<bool>("Ladder", "BlocksInput", true, "True stops your character moving, looking and attacking while the window is open, the same way the inventory does. False leaves the game fully playable behind the window, but then the mouse is shared between looking around and clicking the tabs.");
			LadderScale = config.Bind<float>("Ladder", "Scale", 1f, new ConfigDescription("Size multiplier for the window. The window already scales with screen height; this is for taste.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 2.5f), Array.Empty<object>()));
		}
	}
	[HarmonyPatch]
	internal static class ClientPatches
	{
		private const int LeaseFrames = 1;

		private static int _openOnFrame = -1;

		private static int _blockedOnFrame = -1;

		private static bool WindowOpen
		{
			get
			{
				if (_openOnFrame >= 0)
				{
					return Time.frameCount - _openOnFrame <= 1;
				}
				return false;
			}
		}

		private static bool InputBlocked
		{
			get
			{
				if (_blockedOnFrame >= 0)
				{
					return Time.frameCount - _blockedOnFrame <= 1;
				}
				return false;
			}
		}

		public static void RenewOpen()
		{
			_openOnFrame = Time.frameCount;
		}

		public static void RenewInputBlock()
		{
			_blockedOnFrame = Time.frameCount;
		}

		public static void Release()
		{
			_openOnFrame = -1;
			_blockedOnFrame = -1;
		}

		[HarmonyPostfix]
		[HarmonyPatch(typeof(PlayerController), "TakeInput")]
		private static void PlayerControllerTakeInput(ref bool __result)
		{
			if (InputBlocked)
			{
				__result = false;
			}
		}

		[HarmonyPostfix]
		[HarmonyPatch(typeof(Player), "TakeInput")]
		private static void PlayerTakeInput(Player __instance, ref bool __result)
		{
			if (InputBlocked && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				__result = false;
			}
		}

		[HarmonyPostfix]
		[HarmonyPatch(typeof(GameCamera), "UpdateMouseCapture")]
		private static void GameCameraUpdateMouseCapture()
		{
			if (WindowOpen)
			{
				Cursor.lockState = (CursorLockMode)0;
				Cursor.visible = true;
			}
		}
	}
	[BepInPlugin("com.deathboard.valheim.client", "The Great Hall", "3.0.0")]
	public sealed class DeathboardClientPlugin : BaseUnityPlugin
	{
		private ClientConfig _config;

		private RealmSession _session;

		private LadderClient _ladderClient;

		private LadderGui _ladder;

		private SkillReporter _skills;

		private SnapshotUploader _uploader;

		private RealmProfile _realmProfile;

		private RealmConsentGui _consent;

		private DeathReporter _deaths;

		private StatsReporter _stats;

		private ProfileGui _profile;

		private Harmony _harmony;

		private Harmony _logoutHarmony;

		private Harmony _spawnGateHarmony;

		private Harmony _deathHarmony;

		private Harmony _blockHarmony;

		internal static DeathboardClientPlugin Instance { get; private set; }

		internal static bool IsHeadless => (int)SystemInfo.graphicsDeviceType == 4;

		internal RealmSession Session => _session;

		internal SnapshotUploader Uploader => _uploader;

		internal RealmProfile RealmProfile => _realmProfile;

		internal DeathReporter Deaths => _deaths;

		private void Awake()
		{
			//IL_03ae: Unknown result type (might be due to invalid IL or missing references)
			//IL_03b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_026e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0278: Expected O, but got Unknown
			//IL_02bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c5: Expected O, but got Unknown
			//IL_0308: Unknown result type (might be due to invalid IL or missing references)
			//IL_0312: Expected O, but got Unknown
			//IL_0355: Unknown result type (might be due to invalid IL or missing references)
			//IL_035f: Expected O, but got Unknown
			//IL_0221: Unknown result type (might be due to invalid IL or missing references)
			//IL_022b: Expected O, but got Unknown
			Instance = this;
			LogInfo("===== " + BuildInfo.ClientBanner + " starting =====");
			if (IsHeadless)
			{
				LogInfo("Headless build detected; standing down. The server half of Deathboard runs from Deathboard.dll.");
				return;
			}
			_config = new ClientConfig(((BaseUnityPlugin)this).Config);
			if (!_config.Enabled.Value)
			{
				LogInfo("Disabled by config; doing nothing.");
				return;
			}
			_session = new RealmSession(() => _config.HandshakeTimeoutSeconds.Value, LogInfo, LogWarn);
			_ladderClient = new LadderClient(_session, () => _config.LadderRefreshSeconds.Value, LogWarn);
			_ladder = new LadderGui(_ladderClient, _session, () => _config.LadderKey.Value, () => _config.LadderScale.Value, () => _config.LadderBlocksInput.Value);
			_profile = new ProfileGui(_ladderClient, () => _config.LadderScale.Value);
			_skills = new SkillReporter(_session, LogInfo, LogWarn);
			_deaths = new DeathReporter(_session, LogInfo, LogWarn);
			_stats = new StatsReporter(_session, LogInfo, LogWarn);
			_uploader = new SnapshotUploader(_session, LogInfo, LogWarn);
			_consent = new RealmConsentGui(() => (!((Object)(object)ZNet.instance == (Object)null)) ? ZNet.instance.GetWorldName() : null, LogWarn);
			_realmProfile = new RealmProfile(_session, _consent, LogInfo, LogWarn);
			try
			{
				_harmony = new Harmony("com.deathboard.valheim.client");
				_harmony.PatchAll(typeof(ClientPatches));
			}
			catch (Exception ex)
			{
				LogWarn("Could not install the leaderboard input patches: " + ex.Message + ". The window will still open, but the mouse stays captured.");
				_harmony = null;
			}
			try
			{
				_logoutHarmony = new Harmony("com.deathboard.valheim.client.logout");
				_logoutHarmony.PatchAll(typeof(SnapshotLogoutPatch));
			}
			catch (Exception ex2)
			{
				LogWarn("Could not install the logout upload patch: " + ex2.Message + ". Snapshots still upload on the interval and on death.");
				_logoutHarmony = null;
			}
			try
			{
				_spawnGateHarmony = new Harmony("com.deathboard.valheim.client.spawngate");
				_spawnGateHarmony.PatchAll(typeof(RealmSpawnGate));
			}
			catch (Exception ex3)
			{
				LogWarn("Could not install the spawn gate: " + ex3.Message + ". Realm characters still work; a very slow handshake will disconnect instead of waiting.");
				_spawnGateHarmony = null;
			}
			try
			{
				_deathHarmony = new Harmony("com.deathboard.valheim.client.death");
				_deathHarmony.PatchAll(typeof(DeathReportPatch));
			}
			catch (Exception ex4)
			{
				LogWarn("Could not install the death report patch: " + ex4.Message + ". Deaths are still counted by the server; they just will not say what killed you.");
				_deathHarmony = null;
			}
			try
			{
				_blockHarmony = new Harmony("com.deathboard.valheim.client.block");
				_blockHarmony.PatchAll(typeof(BlockCounterPatch));
			}
			catch (Exception ex5)
			{
				LogWarn("Could not install the block counter: " + ex5.Message + ". Every other stat still reports; blocks and parries will read zero.");
				_blockHarmony = null;
			}
			LogInfo("Ready. Will greet each dedicated server once and go inert if it does not answer. Press " + ((object)_config.LadderKey.Value/*cast due to .constrained prefix*/).ToString() + " in game for the leaderboard.");
		}

		private void Update()
		{
			if (_session == null)
			{
				return;
			}
			try
			{
				_session.Tick();
				if (_ladder != null)
				{
					_ladder.Tick();
				}
				if (_skills != null)
				{
					_skills.Tick();
				}
				if (_stats != null)
				{
					_stats.Tick();
				}
				if (_realmProfile != null)
				{
					_realmProfile.Tick();
				}
				if (_uploader != null)
				{
					_uploader.Tick();
				}
			}
			catch (Exception ex)
			{
				LogWarn("Realm tick failed, standing down: " + ex);
				_session = null;
				_ladder = null;
				_ladderClient = null;
				_profile = null;
				_skills = null;
				_deaths = null;
				_stats = null;
				_uploader = null;
				_realmProfile = null;
				if (_consent != null)
				{
					_consent.Prompt = null;
				}
				ClientPatches.Release();
			}
		}

		private void OnGUI()
		{
			if (_consent != null && _consent.Showing)
			{
				try
				{
					_consent.Draw();
				}
				catch (Exception ex)
				{
					LogWarn("Consent window draw failed: " + ex);
				}
			}
			if (_ladder == null)
			{
				return;
			}
			try
			{
				_ladder.Draw();
			}
			catch (Exception ex2)
			{
				LogWarn("Leaderboard draw failed; closing it: " + ex2);
				_ladder.Close();
			}
			if (_profile == null)
			{
				return;
			}
			if (!LadderGui.IsOpen)
			{
				_profile.Close();
				return;
			}
			try
			{
				_profile.Draw();
			}
			catch (Exception ex3)
			{
				LogWarn("Profile window draw failed; closing it: " + ex3);
				_profile.Close();
			}
		}

		private void OnDestroy()
		{
			Instance = null;
			ClientPatches.Release();
			if (_harmony != null)
			{
				_harmony.UnpatchSelf();
				_harmony = null;
			}
			if (_logoutHarmony != null)
			{
				_logoutHarmony.UnpatchSelf();
				_logoutHarmony = null;
			}
			if (_spawnGateHarmony != null)
			{
				_spawnGateHarmony.UnpatchSelf();
				_spawnGateHarmony = null;
			}
			if (_deathHarmony != null)
			{
				_deathHarmony.UnpatchSelf();
				_deathHarmony = null;
			}
			if (_blockHarmony != null)
			{
				_blockHarmony.UnpatchSelf();
				_blockHarmony = null;
			}
		}

		private void LogInfo(string message)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)("[Deathboard.Client] " + message));
		}

		private void LogWarn(string message)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("[Deathboard.Client] " + message));
		}
	}
	[HarmonyPatch]
	internal static class DeathReportPatch
	{
		private static MethodBase TargetMethod()
		{
			return AccessTools.Method(typeof(Player), "OnDeath", (Type[])null, (Type[])null);
		}

		private static void Postfix(Player __instance)
		{
			try
			{
				DeathboardClientPlugin instance = DeathboardClientPlugin.Instance;
				if (!((Object)(object)instance == (Object)null) && instance.Deaths != null && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
				{
					instance.Deaths.ReportDeath(__instance);
				}
			}
			catch (Exception)
			{
			}
		}
	}
	internal sealed class DeathReporter
	{
		private static readonly FieldInfo LastHitField = AccessTools.Field(typeof(Character), "m_lastHit");

		private const float MinReportIntervalSeconds = 2f;

		private readonly RealmSession _session;

		private readonly Action<string> _logInfo;

		private readonly Action<string> _logWarning;

		private float _lastSentAt = -999f;

		private bool _announced;

		public bool Available
		{
			get
			{
				if (_session != null && _session.Server.AcceptsDeathReports)
				{
					return _session.BoardAvailable;
				}
				return false;
			}
		}

		public DeathReporter(RealmSession session, Action<string> logInfo, Action<string> logWarning)
		{
			_session = session;
			_logInfo = logInfo;
			_logWarning = logWarning;
		}

		public void ReportDeath(Player player)
		{
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Expected I4, but got Unknown
			if (!Available || (Object)(object)player == (Object)null)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - _lastSentAt < 2f)
			{
				return;
			}
			string text = LocalSteamId();
			string text2 = LocalCharacterName();
			if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(text2))
			{
				byte hitType = 0;
				string key = "";
				string display = "";
				bool isPlayer = false;
				HitData val = ReadLastHit(player);
				if (val != null)
				{
					hitType = (byte)(int)val.m_hitType;
					Describe(val, out key, out display, out isPlayer);
				}
				try
				{
					ZRoutedRpc.instance.InvokeRoutedRPC("Deathboard_DeathReport", new object[1] { RealmProtocol.WriteDeathReport(text, text2, hitType, key, display, isPlayer) });
				}
				catch (Exception ex)
				{
					_logWarning("Could not send death report: " + ex.Message);
					return;
				}
				_lastSentAt = realtimeSinceStartup;
				if (!_announced)
				{
					_announced = true;
					_logInfo("Reporting what killed you to the server, so the death line and the Killed By list can say. Your death count itself is measured by the server, not sent by you.");
				}
			}
		}

		private HitData ReadLastHit(Player player)
		{
			if (LastHitField == null)
			{
				return null;
			}
			try
			{
				object? value = LastHitField.GetValue(player);
				return (HitData)((value is HitData) ? value : null);
			}
			catch (Exception)
			{
				return null;
			}
		}

		private static void Describe(HitData hit, out string key, out string display, out bool isPlayer)
		{
			key = "";
			display = "";
			isPlayer = false;
			try
			{
				if (!hit.HaveAttacker())
				{
					return;
				}
				Character attacker = hit.GetAttacker();
				if ((Object)(object)attacker == (Object)null)
				{
					return;
				}
				if (attacker.IsPlayer())
				{
					Player val = (Player)(object)((attacker is Player) ? attacker : null);
					string text = (((Object)(object)val != (Object)null) ? val.GetPlayerName() : attacker.m_name);
					if (!string.IsNullOrEmpty(text))
					{
						isPlayer = true;
						key = text;
						display = text;
					}
				}
				else
				{
					key = PrefabName(attacker);
					display = attacker.GetHoverName();
					if (string.IsNullOrEmpty(key))
					{
						key = display;
					}
				}
			}
			catch (Exception)
			{
			}
		}

		private static string PrefabName(Character character)
		{
			string text = (((Object)(object)((Component)character).gameObject == (Object)null) ? "" : ((Object)((Component)character).gameObject).name);
			if (string.IsNullOrEmpty(text))
			{
				return "";
			}
			int num = text.IndexOf("(Clone)", StringComparison.Ordinal);
			return ((num >= 0) ? text.Substring(0, num) : text).Trim();
		}

		private static string LocalCharacterName()
		{
			Game instance = Game.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return null;
			}
			PlayerProfile playerProfile = instance.GetPlayerProfile();
			if (playerProfile != null)
			{
				return playerProfile.GetName();
			}
			return null;
		}

		private static string LocalSteamId()
		{
			//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)
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				PlatformUserID platformUserID = ((IUser)PlatformManager.DistributionPlatform.LocalUser).PlatformUserID;
				return ((PlatformUserID)(ref platformUserID)).IsValid ? platformUserID.m_userID : null;
			}
			catch (Exception)
			{
				return null;
			}
		}
	}
	internal static class GuiTextures
	{
		public static Texture2D Solid(Color color)
		{
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			//IL_0009: Unknown result type (might be due to invalid IL or missing

Deathboard.dll

Decompiled a month ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Splatform;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("Deathboard")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Server-only death ledger and leaderboard for a Valheim dedicated server.")]
[assembly: AssemblyFileVersion("3.0.0.0")]
[assembly: AssemblyInformationalVersion("3.0.0+50a58f88d65ce17edb1ecef1547196a134bc4c9c")]
[assembly: AssemblyProduct("Deathboard")]
[assembly: AssemblyTitle("Deathboard")]
[assembly: AssemblyVersion("3.0.0.0")]
namespace Deathboard;

internal enum SpeakerAttribution
{
	Subject,
	Recipient
}
internal sealed class Announcer
{
	private readonly DeathboardConfig _config;

	private readonly DeathWatch _watch;

	private readonly Action<string> _logWarning;

	public Announcer(DeathboardConfig config, DeathWatch watch, Action<string> logWarning)
	{
		_config = config;
		_watch = watch;
		_logWarning = logWarning;
	}

	public string Tag(string message)
	{
		string value = _config.AnnouncementName.Value;
		if (!string.IsNullOrEmpty(value))
		{
			return "[" + value + "] " + message;
		}
		return message;
	}

	public void Say(PeerCharacter attributeTo, string message, int? talkTypeOverride = null)
	{
		Say(attributeTo, new List<string> { message }, talkTypeOverride);
	}

	public void SayTo(PeerCharacter recipient, string message, int? talkTypeOverride = null)
	{
		//IL_0029: Unknown result type (might be due to invalid IL or missing references)
		if (recipient != null && !string.IsNullOrEmpty(message) && DeathWatch.ServerActive() && ((PlatformUserID)(ref recipient.PlatformId)).IsValid)
		{
			VanillaChat.Send(recipient.PeerId, recipient.PlatformId, recipient.Name, talkTypeOverride ?? _config.ChatType.Value, message, _config.SuppressFloatingText.Value);
		}
	}

	public void Say(PeerCharacter attributeTo, List<string> messages, int? talkTypeOverride = null)
	{
		//IL_010f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0083: Unknown result type (might be due to invalid IL or missing references)
		if (messages == null || messages.Count == 0 || !DeathWatch.ServerActive())
		{
			return;
		}
		int talkType = talkTypeOverride ?? _config.ChatType.Value;
		bool value = _config.SuppressFloatingText.Value;
		if (_config.Attribution.Value == SpeakerAttribution.Recipient)
		{
			foreach (PeerCharacter item in Snapshot())
			{
				for (int i = 0; i < messages.Count; i++)
				{
					VanillaChat.Send(item.PeerId, item.PlatformId, item.Name, talkType, messages[i], value);
				}
			}
			return;
		}
		PeerCharacter peerCharacter = attributeTo ?? FirstOnline();
		if (peerCharacter == null)
		{
			return;
		}
		if (!((PlatformUserID)(ref peerCharacter.PlatformId)).IsValid)
		{
			_logWarning("No valid PlatformUserID for " + peerCharacter.Name + "; skipping chat broadcast. The client would drop the message.");
			return;
		}
		for (int j = 0; j < messages.Count; j++)
		{
			VanillaChat.Send(0L, peerCharacter.PlatformId, peerCharacter.Name, talkType, messages[j], value);
		}
	}

	private List<PeerCharacter> Snapshot()
	{
		List<PeerCharacter> list = new List<PeerCharacter>();
		foreach (PeerCharacter item in _watch.Online)
		{
			if (((PlatformUserID)(ref item.PlatformId)).IsValid)
			{
				list.Add(item);
			}
		}
		return list;
	}

	private PeerCharacter FirstOnline()
	{
		foreach (PeerCharacter item in _watch.Online)
		{
			if (((PlatformUserID)(ref item.PlatformId)).IsValid)
			{
				return item;
			}
		}
		return null;
	}
}
internal enum BoardSort
{
	Ascending,
	Descending
}
internal enum BoardKind
{
	Deaths,
	Power,
	Combat,
	Acro,
	Hand,
	Who
}
internal static class Board
{
	public const string DisplayName = "Deathboard";

	public const string BoardTitle = "The Great Hall";

	public const int HeaderLineCount = 1;

	public static BoardSort DefaultSort(BoardKind kind)
	{
		if (kind != BoardKind.Deaths)
		{
			return BoardSort.Descending;
		}
		return BoardSort.Ascending;
	}

	public static string SafeName(string name)
	{
		if (string.IsNullOrEmpty(name))
		{
			return "?";
		}
		StringBuilder stringBuilder = new StringBuilder(name.Length);
		foreach (char c in name)
		{
			stringBuilder.Append((c < ' ' || c == '\u007f') ? ' ' : c);
		}
		return stringBuilder.ToString();
	}

	public static string Header(BoardKind kind, BoardSort sort, string[] labels, string title = null)
	{
		string text = (string.IsNullOrEmpty(title) ? "The Great Hall" : title);
		if (kind == BoardKind.Deaths && sort == BoardSort.Ascending)
		{
			return text;
		}
		string[] array = ((labels != null && labels.Length == 3) ? labels : PowerRating.ParseLabels("Combat,Agility,Trades"));
		bool flag = sort == BoardSort.Descending;
		return text + " (" + kind switch
		{
			BoardKind.Deaths => "most deaths", 
			BoardKind.Power => (flag ? "highest" : "lowest") + " power", 
			BoardKind.Combat => (flag ? "highest" : "lowest") + " " + array[0], 
			BoardKind.Acro => (flag ? "highest" : "lowest") + " " + array[1], 
			BoardKind.Hand => (flag ? "highest" : "lowest") + " " + array[2], 
			BoardKind.Who => "online", 
			_ => "all", 
		} + ")";
	}

	private static float StatOf(DeathRecord r, BoardKind kind, PowerWeights weights)
	{
		SkillProfile skills = r.Skills;
		switch (kind)
		{
		case BoardKind.Combat:
			if (!skills.HasData)
			{
				return 0f;
			}
			return skills.Combat;
		case BoardKind.Acro:
			if (!skills.HasData)
			{
				return 0f;
			}
			return skills.Acro;
		case BoardKind.Hand:
			if (!skills.HasData)
			{
				return 0f;
			}
			return skills.Hand;
		default:
			return PowerRating.Compute(skills, weights);
		}
	}

	public static List<DeathRecord> Sort(List<DeathRecord> records, BoardKind kind, BoardSort sort, PowerWeights weights)
	{
		List<DeathRecord> list = new List<DeathRecord>(records);
		list.Sort(delegate(DeathRecord a, DeathRecord b)
		{
			if (kind == BoardKind.Deaths)
			{
				int num = ((sort == BoardSort.Descending) ? b.Deaths.CompareTo(a.Deaths) : a.Deaths.CompareTo(b.Deaths));
				if (num != 0)
				{
					return num;
				}
				int num2 = PowerRating.Compute(b.Skills, weights).CompareTo(PowerRating.Compute(a.Skills, weights));
				if (num2 != 0)
				{
					return num2;
				}
			}
			else
			{
				float value = StatOf(a, kind, weights);
				float value2 = StatOf(b, kind, weights);
				int num = ((sort == BoardSort.Descending) ? value2.CompareTo(value) : value.CompareTo(value2));
				if (num != 0)
				{
					return num;
				}
				int num3 = a.Deaths.CompareTo(b.Deaths);
				if (num3 != 0)
				{
					return num3;
				}
			}
			int num4 = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
			return (num4 != 0) ? num4 : string.CompareOrdinal(a.SteamId, b.SteamId);
		});
		return list;
	}

	public static List<DeathRecord> Select(List<DeathRecord> records, BoardKind kind, BoardSort sort, PowerWeights weights, Func<DeathRecord, bool> isOnline)
	{
		List<DeathRecord> list = new List<DeathRecord>();
		for (int i = 0; i < records.Count; i++)
		{
			if (kind != BoardKind.Who || (isOnline != null && isOnline(records[i])))
			{
				list.Add(records[i]);
			}
		}
		return Sort(list, kind, sort, weights);
	}

	public static List<string> Render(List<DeathRecord> records, BoardKind kind, BoardSort sort, BoardOptions options, Func<DeathRecord, bool> isOnline)
	{
		List<string> list = new List<string>();
		List<DeathRecord> list2 = Select(records, kind, sort, options.Weights, isOnline);
		list.Add(Header(kind, sort, options.Labels, options.Title));
		if (list2.Count == 0)
		{
			list.Add((kind == BoardKind.Who) ? "(nobody is online)" : "(no vikings recorded yet)");
			return list;
		}
		int num = Math.Max(1, options.LinesPerMessage);
		StringBuilder stringBuilder = new StringBuilder();
		int num2 = 0;
		for (int i = 0; i < list2.Count; i++)
		{
			DeathRecord deathRecord = list2[i];
			if (num2 > 0)
			{
				stringBuilder.Append('\n');
			}
			string text = SafeName(deathRecord.Name);
			if (options.MarkOnline && isOnline != null && isOnline(deathRecord))
			{
				text = "*" + text;
			}
			stringBuilder.Append(PowerRating.Line(options.Format, text, deathRecord.Deaths, deathRecord.Skills, options.Weights, options.ShowPower));
			num2++;
			if (num2 >= num)
			{
				list.Add(stringBuilder.ToString());
				stringBuilder.Length = 0;
				num2 = 0;
			}
		}
		if (num2 > 0)
		{
			list.Add(stringBuilder.ToString());
		}
		return list;
	}

	public static string Usage(string prefix)
	{
		return prefix + "deathboard [asc|desc]  " + prefix + "power  " + prefix + "com  " + prefix + "acro  " + prefix + "hand  " + prefix + "who";
	}
}
internal sealed class BoardOptions
{
	public string Format = "{name} — {deaths}   {P} ({C},{A},{H})";

	public string[] Labels = PowerRating.ParseLabels("Combat,Agility,Trades");

	public PowerWeights Weights = PowerWeights.Default;

	public bool ShowPower = true;

	public bool MarkOnline;

	public int LinesPerMessage = 8;

	public string Title = "The Great Hall";
}
internal sealed class BossRoster
{
	private const float RescanIntervalSeconds = 60f;

	private readonly DeathboardConfig _config;

	private readonly Action<string> _logInfo;

	private readonly Action<string> _logWarning;

	private readonly List<KeyValuePair<string, string>> _roster = new List<KeyValuePair<string, string>>();

	private float _lastScan = -999f;

	private bool _warnedNoScene;

	private string _lastLoggedShape = "";

	public BossRoster(DeathboardConfig config, Action<string> logInfo, Action<string> logWarning)
	{
		_config = config;
		_logInfo = logInfo;
		_logWarning = logWarning;
	}

	public List<BossTick> Build()
	{
		List<BossTick> list = new List<BossTick>();
		if (_config != null && !_config.ShowBossRow.Value)
		{
			return list;
		}
		EnsureRoster();
		ZoneSystem instance = ZoneSystem.instance;
		for (int i = 0; i < _roster.Count && i < 32; i++)
		{
			string key = _roster[i].Key;
			bool down = false;
			try
			{
				if ((Object)(object)instance != (Object)null)
				{
					down = instance.GetGlobalKey(key);
				}
			}
			catch (Exception)
			{
			}
			list.Add(new BossTick
			{
				Key = key,
				Name = _roster[i].Value,
				Down = down
			});
		}
		return list;
	}

	private void EnsureRoster()
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (_roster.Count > 0 && realtimeSinceStartup - _lastScan < 60f)
		{
			return;
		}
		_lastScan = realtimeSinceStartup;
		List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
		HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
		foreach (KeyValuePair<string, string> item in ParseOrder(_config.BossOrder.Value))
		{
			if (hashSet.Add(item.Key))
			{
				list.Add(item);
			}
		}
		List<KeyValuePair<string, string>> list2 = ScanPrefabs();
		List<string> list3 = new List<string>();
		for (int i = 0; i < list2.Count; i++)
		{
			if (hashSet.Add(list2[i].Key))
			{
				list.Add(list2[i]);
				list3.Add(list2[i].Key);
			}
		}
		_roster.Clear();
		_roster.AddRange(list);
		string text = string.Join(",", Keys(_roster).ToArray());
		if (!string.Equals(text, _lastLoggedShape, StringComparison.Ordinal))
		{
			_lastLoggedShape = text;
			_logInfo("Boss roster (" + _roster.Count + "): " + text);
			if (list3.Count > 0)
			{
				_logWarning("These bosses exist in the game but are not in BossOrder, so they are appended at the end and may be out of progression order: " + string.Join(", ", list3.ToArray()) + ". Add them to BossOrder to place them.");
			}
		}
	}

	private static List<string> Keys(List<KeyValuePair<string, string>> list)
	{
		List<string> list2 = new List<string>(list.Count);
		for (int i = 0; i < list.Count; i++)
		{
			list2.Add(list[i].Key);
		}
		return list2;
	}

	private List<KeyValuePair<string, string>> ScanPrefabs()
	{
		List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
		ZNetScene instance = ZNetScene.instance;
		if ((Object)(object)instance == (Object)null || instance.m_prefabs == null)
		{
			if (!_warnedNoScene)
			{
				_warnedNoScene = true;
				_logInfo("ZNetScene is not up yet; the boss row uses the configured order until it is.");
			}
			return list;
		}
		try
		{
			for (int i = 0; i < instance.m_prefabs.Count; i++)
			{
				GameObject val = instance.m_prefabs[i];
				if ((Object)(object)val == (Object)null)
				{
					continue;
				}
				Character component = val.GetComponent<Character>();
				if (!((Object)(object)component == (Object)null) && component.m_boss)
				{
					string defeatSetGlobalKey = component.m_defeatSetGlobalKey;
					if (!string.IsNullOrEmpty(defeatSetGlobalKey))
					{
						list.Add(new KeyValuePair<string, string>(defeatSetGlobalKey, PrettyBossName(defeatSetGlobalKey, ((Object)val).name)));
					}
				}
			}
		}
		catch (Exception ex)
		{
			_logWarning("Boss prefab scan failed: " + ex.Message + ". Falling back to the configured order.");
		}
		return list;
	}

	private static string PrettyBossName(string key, string prefabName)
	{
		string text = key ?? "";
		if (text.StartsWith("defeated_", StringComparison.OrdinalIgnoreCase))
		{
			text = text.Substring("defeated_".Length);
		}
		if (text.Length == 0)
		{
			if (!string.IsNullOrEmpty(prefabName))
			{
				return prefabName;
			}
			return "?";
		}
		return char.ToUpperInvariant(text[0]) + text.Substring(1);
	}

	private static List<KeyValuePair<string, string>> ParseOrder(string raw)
	{
		List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();
		if (string.IsNullOrEmpty(raw))
		{
			return list;
		}
		string[] array = raw.Split(new char[1] { ',' });
		for (int i = 0; i < array.Length; i++)
		{
			string text = array[i].Trim();
			if (text.Length == 0)
			{
				continue;
			}
			int num = text.IndexOf('=');
			string text2 = ((num >= 0) ? text.Substring(0, num).Trim() : text);
			string text3 = ((num >= 0) ? text.Substring(num + 1).Trim() : "");
			if (text2.Length != 0)
			{
				if (text3.Length == 0)
				{
					text3 = PrettyBossName(text2, text2);
				}
				list.Add(new KeyValuePair<string, string>(text2, text3));
			}
		}
		return list;
	}
}
internal sealed class CauseTally
{
	public string Key = "";

	public string Display = "";

	public int Count;

	public string LastUtc = "";
}
internal sealed class RecentDeath
{
	public string Utc = "";

	public int Day;

	public string Key = "";

	public string Display = "";
}
internal sealed class CauseRecord
{
	public string SteamId = "";

	public string Name = "";

	public readonly Dictionary<string, CauseTally> Causes = new Dictionary<string, CauseTally>(StringComparer.Ordinal);

	public readonly List<RecentDeath> Recent = new List<RecentDeath>();

	public int Attributed
	{
		get
		{
			int num = 0;
			foreach (CauseTally value in Causes.Values)
			{
				num += value.Count;
			}
			return num;
		}
	}

	public string Key => DeathLedger.MakeKey(SteamId, Name);

	public List<CauseTally> Sorted()
	{
		List<CauseTally> list = new List<CauseTally>(Causes.Values);
		list.Sort((CauseTally a, CauseTally b) => (a.Count != b.Count) ? b.Count.CompareTo(a.Count) : string.Compare(DeathCause.ShortLabel(a.Key, a.Display), DeathCause.ShortLabel(b.Key, b.Display), StringComparison.OrdinalIgnoreCase));
		return list;
	}
}
internal sealed class CauseStore
{
	private const int FileVersion = 1;

	public const int RecentCap = 20;

	public const int MaxCausesPerCharacter = 256;

	private readonly Dictionary<string, CauseRecord> _records = new Dictionary<string, CauseRecord>(StringComparer.Ordinal);

	private readonly string _path;

	private readonly Action<string> _logInfo;

	private readonly Action<string> _logError;

	public string Path => _path;

	public int Count => _records.Count;

	public CauseStore(string path, Action<string> logInfo, Action<string> logError)
	{
		_path = path;
		_logInfo = logInfo;
		_logError = logError;
	}

	private static string UtcNow()
	{
		return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
	}

	public bool TryGet(string steamId, string name, out CauseRecord record)
	{
		return _records.TryGetValue(DeathLedger.MakeKey(steamId, name), out record);
	}

	public bool TryGetByName(string name, out CauseRecord record)
	{
		record = null;
		if (string.IsNullOrEmpty(name))
		{
			return false;
		}
		foreach (CauseRecord value in _records.Values)
		{
			if (string.Equals(value.Name, name, StringComparison.Ordinal))
			{
				record = value;
				return true;
			}
		}
		return false;
	}

	public CauseRecord Ensure(string steamId, string name)
	{
		string key = DeathLedger.MakeKey(steamId, name);
		if (_records.TryGetValue(key, out var value))
		{
			return value;
		}
		value = new CauseRecord
		{
			SteamId = (steamId ?? ""),
			Name = (name ?? "")
		};
		_records[key] = value;
		return value;
	}

	public bool Record(string steamId, string name, string causeKey, string display, int worldDay)
	{
		causeKey = DeathCause.Trim(causeKey);
		if (string.IsNullOrEmpty(causeKey))
		{
			return false;
		}
		CauseRecord causeRecord = Ensure(steamId, name);
		string text = UtcNow();
		if (!causeRecord.Causes.TryGetValue(causeKey, out var value))
		{
			if (causeRecord.Causes.Count >= 256)
			{
				return false;
			}
			value = new CauseTally
			{
				Key = causeKey
			};
			causeRecord.Causes[causeKey] = value;
		}
		value.Count++;
		value.LastUtc = text;
		string text2 = DeathCause.Trim(display);
		if (!string.IsNullOrEmpty(text2))
		{
			value.Display = text2;
		}
		causeRecord.Recent.Insert(0, new RecentDeath
		{
			Utc = text,
			Day = ((worldDay >= 0) ? worldDay : 0),
			Key = causeKey,
			Display = value.Display
		});
		while (causeRecord.Recent.Count > 20)
		{
			causeRecord.Recent.RemoveAt(causeRecord.Recent.Count - 1);
		}
		return true;
	}

	public List<CauseRecord> Snapshot()
	{
		return new List<CauseRecord>(_records.Values);
	}

	public void Load()
	{
		try
		{
			string directoryName = System.IO.Path.GetDirectoryName(_path);
			if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			if (!File.Exists(_path))
			{
				_logInfo("No cause file at " + _path + " yet; starting a fresh one.");
				Save();
				return;
			}
			string text = File.ReadAllText(_path, Encoding.UTF8);
			if (string.IsNullOrEmpty(text.Trim()))
			{
				_logInfo("Cause file at " + _path + " was empty; starting a fresh one.");
				return;
			}
			object value;
			List<object> obj = MiniJson.AsArray((MiniJson.AsObject(MiniJson.Parse(text)) ?? throw new FormatException("Top-level value is not a JSON object.")).TryGetValue("entries", out value) ? value : null) ?? throw new FormatException("Missing 'entries' array.");
			_records.Clear();
			foreach (object item in obj)
			{
				Dictionary<string, object> dictionary = MiniJson.AsObject(item);
				if (dictionary == null)
				{
					continue;
				}
				CauseRecord causeRecord = new CauseRecord
				{
					SteamId = (MiniJson.GetString(dictionary, "steamId", "") ?? ""),
					Name = (MiniJson.GetString(dictionary, "name", "") ?? "")
				};
				if (causeRecord.Name.Length == 0)
				{
					continue;
				}
				object value2;
				List<object> list = MiniJson.AsArray(dictionary.TryGetValue("causes", out value2) ? value2 : null);
				if (list != null)
				{
					foreach (object item2 in list)
					{
						Dictionary<string, object> dictionary2 = MiniJson.AsObject(item2);
						if (dictionary2 == null)
						{
							continue;
						}
						string text2 = DeathCause.Trim(MiniJson.GetString(dictionary2, "key", ""));
						if (text2.Length != 0)
						{
							int num = MiniJson.GetInt(dictionary2, "count");
							if (num > 0)
							{
								causeRecord.Causes[text2] = new CauseTally
								{
									Key = text2,
									Display = DeathCause.Trim(MiniJson.GetString(dictionary2, "display", "")),
									Count = num,
									LastUtc = (MiniJson.GetString(dictionary2, "lastUtc", "") ?? "")
								};
							}
						}
					}
				}
				object value3;
				List<object> list2 = MiniJson.AsArray(dictionary.TryGetValue("recent", out value3) ? value3 : null);
				if (list2 != null)
				{
					foreach (object item3 in list2)
					{
						Dictionary<string, object> dictionary3 = MiniJson.AsObject(item3);
						if (dictionary3 == null)
						{
							continue;
						}
						string text3 = DeathCause.Trim(MiniJson.GetString(dictionary3, "key", ""));
						if (text3.Length != 0)
						{
							causeRecord.Recent.Add(new RecentDeath
							{
								Utc = (MiniJson.GetString(dictionary3, "utc", "") ?? ""),
								Day = Math.Max(0, MiniJson.GetInt(dictionary3, "day")),
								Key = text3,
								Display = DeathCause.Trim(MiniJson.GetString(dictionary3, "display", ""))
							});
							if (causeRecord.Recent.Count >= 20)
							{
								break;
							}
						}
					}
				}
				_records[causeRecord.Key] = causeRecord;
			}
			_logInfo("Loaded death causes for " + _records.Count + " character(s) from " + _path + ".");
		}
		catch (Exception ex)
		{
			_logError("Failed to load cause file '" + _path + "': " + ex.Message + " - starting empty and leaving the existing file untouched until the next write.");
		}
	}

	public void Save()
	{
		try
		{
			string directoryName = System.IO.Path.GetDirectoryName(_path);
			if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			List<CauseRecord> list = Snapshot();
			list.Sort(delegate(CauseRecord a, CauseRecord b)
			{
				int num4 = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
				return (num4 == 0) ? string.CompareOrdinal(a.SteamId, b.SteamId) : num4;
			});
			StringBuilder stringBuilder = new StringBuilder(256 + list.Count * 256);
			stringBuilder.Append("{\n  \"version\": ").Append(1).Append(",\n  \"entries\": [\n");
			for (int num = 0; num < list.Count; num++)
			{
				CauseRecord causeRecord = list[num];
				stringBuilder.Append("    { \"steamId\": ");
				MiniJson.WriteString(stringBuilder, causeRecord.SteamId);
				stringBuilder.Append(", \"name\": ");
				MiniJson.WriteString(stringBuilder, causeRecord.Name);
				stringBuilder.Append(", \"causes\": [");
				List<CauseTally> list2 = causeRecord.Sorted();
				for (int num2 = 0; num2 < list2.Count; num2++)
				{
					CauseTally causeTally = list2[num2];
					if (num2 > 0)
					{
						stringBuilder.Append(", ");
					}
					stringBuilder.Append("{ \"key\": ");
					MiniJson.WriteString(stringBuilder, causeTally.Key);
					stringBuilder.Append(", \"display\": ");
					MiniJson.WriteString(stringBuilder, causeTally.Display);
					stringBuilder.Append(", \"count\": ").Append(causeTally.Count.ToString(CultureInfo.InvariantCulture));
					stringBuilder.Append(", \"lastUtc\": ");
					MiniJson.WriteString(stringBuilder, causeTally.LastUtc);
					stringBuilder.Append(" }");
				}
				stringBuilder.Append("], \"recent\": [");
				for (int num3 = 0; num3 < causeRecord.Recent.Count; num3++)
				{
					RecentDeath recentDeath = causeRecord.Recent[num3];
					if (num3 > 0)
					{
						stringBuilder.Append(", ");
					}
					stringBuilder.Append("{ \"utc\": ");
					MiniJson.WriteString(stringBuilder, recentDeath.Utc);
					stringBuilder.Append(", \"day\": ").Append(recentDeath.Day.ToString(CultureInfo.InvariantCulture));
					stringBuilder.Append(", \"key\": ");
					MiniJson.WriteString(stringBuilder, recentDeath.Key);
					stringBuilder.Append(", \"display\": ");
					MiniJson.WriteString(stringBuilder, recentDeath.Display);
					stringBuilder.Append(" }");
				}
				stringBuilder.Append("] }");
				if (num < list.Count - 1)
				{
					stringBuilder.Append(',');
				}
				stringBuilder.Append('\n');
			}
			stringBuilder.Append("  ]\n}\n");
			string text = _path + ".tmp";
			File.WriteAllText(text, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			if (File.Exists(_path))
			{
				File.Delete(_path);
			}
			File.Move(text, _path);
		}
		catch (Exception ex)
		{
			_logError("Failed to save cause file '" + _path + "': " + ex.Message);
		}
	}
}
internal sealed class DeathboardConfig
{
	public readonly ConfigEntry<string> AnnouncementName;

	public readonly ConfigEntry<bool> ArriveAnnounce;

	public readonly ConfigEntry<string> ArriveHint;

	public readonly ConfigEntry<bool> PrintBoardOnArrive;

	public readonly ConfigEntry<bool> DeathAnnounce;

	public readonly ConfigEntry<string> BoardTitle;

	public readonly ConfigEntry<string> CommandPrefix;

	public readonly ConfigEntry<int> LinesPerShout;

	public readonly ConfigEntry<float> JoinDebounceSeconds;

	public readonly ConfigEntry<float> RebroadcastSeconds;

	public readonly ConfigEntry<bool> RebroadcastOnDeath;

	public readonly ConfigEntry<string> DiscordWebhookUrl;

	public readonly ConfigEntry<float> PollIntervalSeconds;

	public readonly ConfigEntry<int> ChatType;

	public readonly ConfigEntry<int> DeathChatType;

	public readonly ConfigEntry<SpeakerAttribution> Attribution;

	public readonly ConfigEntry<bool> MarkOnlinePlayers;

	public readonly ConfigEntry<bool> SuppressFloatingText;

	public readonly ConfigEntry<bool> ChatCommandsEnabled;

	public readonly ConfigEntry<bool> SoloCommands;

	public readonly ConfigEntry<bool> AnnounceBuildOnJoin;

	public readonly ConfigEntry<bool> RealmMode;

	public readonly ConfigEntry<bool> KickUnmodded;

	public readonly ConfigEntry<float> KickUnmoddedGraceSeconds;

	public readonly ConfigEntry<bool> RemoteBoardEnabled;

	public readonly ConfigEntry<bool> AcceptSkillReports;

	public readonly ConfigEntry<bool> AcceptDeathReports;

	public readonly ConfigEntry<bool> AcceptStats;

	public readonly ConfigEntry<bool> ShowBossRow;

	public readonly ConfigEntry<string> BossOrder;

	public readonly ConfigEntry<bool> DeathCauseInAnnounce;

	public readonly ConfigEntry<float> DeathAnnounceDelaySeconds;

	public readonly ConfigEntry<float> DeathCauseWindowSeconds;

	public readonly ConfigEntry<bool> AcceptCharUploads;

	public readonly ConfigEntry<string> SnapshotDir;

	public readonly ConfigEntry<int> BackupCount;

	public readonly ConfigEntry<float> BackupIntervalMinutes;

	public readonly ConfigEntry<float> SkillJumpWarnPoints;

	public readonly ConfigEntry<string> Format;

	public readonly ConfigEntry<string> PowerWeightsRaw;

	public readonly ConfigEntry<string> Labels;

	public readonly ConfigEntry<bool> ShowPowerOnName;

	public readonly ConfigEntry<float> SkillRefreshSeconds;

	public readonly ConfigEntry<SkillsSourceMode> SkillsSource;

	public readonly ConfigEntry<string> CharacterFilesPath;

	public DeathboardConfig(ConfigFile config)
	{
		//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e3: Expected O, but got Unknown
		//IL_0112: Unknown result type (might be due to invalid IL or missing references)
		//IL_011c: Expected O, but got Unknown
		//IL_014b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0155: Expected O, but got Unknown
		//IL_01c0: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ca: Expected O, but got Unknown
		//IL_01f9: Unknown result type (might be due to invalid IL or missing references)
		//IL_0203: Expected O, but got Unknown
		//IL_0232: Unknown result type (might be due to invalid IL or missing references)
		//IL_023c: Expected O, but got Unknown
		//IL_034b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0355: Expected O, but got Unknown
		//IL_0504: Unknown result type (might be due to invalid IL or missing references)
		//IL_050e: Expected O, but got Unknown
		//IL_05d9: Unknown result type (might be due to invalid IL or missing references)
		//IL_05e3: Expected O, but got Unknown
		AnnouncementName = config.Bind<string>("General", "AnnouncementName", "Server", "Tag put in front of server-authored lines, e.g. \"[Server] Bjorn has died (3 deaths)\". Set to an empty string for no tag. NOTE: current Valheim renders the chat speaker name from the server's player list, not from the message, so this cannot replace the speaker name shown before the colon. See README.md.");
		ArriveAnnounce = config.Bind<bool>("General", "ArriveAnnounce", true, "Announce in chat when a player finishes joining.");
		ArriveHint = config.Bind<string>("General", "ArriveHint", "F7 opens the leaderboard.", "Sent privately to a player when they arrive, so they know how to open the window. Only that player sees it. Empty disables it. Change the key here if you change the client's Ladder/Key setting - the server has no way to know what you bound.");
		PrintBoardOnArrive = config.Bind<bool>("General", "PrintBoardOnArrive", false, "Print the whole roster in chat when a player joins. Off by default now that the client's leaderboard window exists - dumping every character into chat on each join is a lot of noise for something a keypress shows better. Turn it on if your players do not run the client mod.");
		DeathAnnounce = config.Bind<bool>("General", "DeathAnnounce", true, "Announce in global chat when a tracked character dies.");
		CommandPrefix = config.Bind<string>("General", "CommandPrefix", "!", "Prefix for chat commands, e.g. \"!\" gives !deathboard and !vikings.");
		LinesPerShout = config.Bind<int>("General", "LinesPerShout", 8, new ConfigDescription("How many names to pack into one chat message when printing the board.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20), Array.Empty<object>()));
		JoinDebounceSeconds = config.Bind<float>("General", "JoinDebounceSeconds", 15f, new ConfigDescription("Minimum seconds between join announcements for the same steamId|character, so a disconnect stutter does not spam the board.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 600f), Array.Empty<object>()));
		RebroadcastSeconds = config.Bind<float>("General", "RebroadcastSeconds", 0f, new ConfigDescription("Reprint the leaderboard in global chat every N seconds while at least one player is online. 0 disables it, which is the default now that the client has a leaderboard window - a board reprinting itself into chat on a timer is noise when a keypress shows it better. 3600 (once an hour) if you want one.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 86400f), Array.Empty<object>()));
		RebroadcastOnDeath = config.Bind<bool>("General", "RebroadcastOnDeath", false, "Also reprint the leaderboard right after a death announcement. An alternative to RebroadcastSeconds if you would rather the board only show up when it changes.");
		DiscordWebhookUrl = config.Bind<string>("General", "DiscordWebhookUrl", "", "Optional Discord webhook URL. Empty disables it. When set, Deathboard POSTs {\"content\":\"...\"} on death and on join.");
		PollIntervalSeconds = config.Bind<float>("Advanced", "PollIntervalSeconds", 1.5f, new ConfigDescription("How often the server polls connected peers for the character ZDO dead flag. Deaths are usually caught instantly by the routed OnDeath RPC; this is the fallback.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 10f), Array.Empty<object>()));
		ChatType = config.Bind<int>("Advanced", "ChatType", 1, new ConfigDescription("Talker.Type for ordinary announcements - the build banner, arrivals, the key hint, command output. 0 whisper, 1 normal, 2 shout. 1 is normal-case white text; 2 renders YELLOW UPPERCASE like a player shout. Deaths have their own setting, DeathChatType. NOTE: this only changes how the line LOOKS. Everything Deathboard sends goes to every connected player over the routed ChatMessage RPC regardless of type, so a 'normal' line is not quieter in reach, only in appearance.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[3] { 0, 1, 2 }), Array.Empty<object>()));
		DeathChatType = config.Bind<int>("Advanced", "DeathChatType", 2, new ConfigDescription("Talker.Type for death announcements specifically. 2 (shout) renders them in yellow uppercase so a death stands out from the rest of the chatter. Same caveat as ChatType: everyone receives it either way, this only changes how loud it looks.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[3] { 0, 1, 2 }), Array.Empty<object>()));
		Attribution = config.Bind<SpeakerAttribution>("Advanced", "Attribution", SpeakerAttribution.Subject, "Whose PlatformUserID server lines are sent under. Subject: one broadcast, attributed to the player the line is about (or who asked). Recipient: one message per player, each attributed to that player themselves - cannot be dropped by a mute, but everyone sees their own name in front of every line.");
		MarkOnlinePlayers = config.Bind<bool>("Advanced", "MarkOnlinePlayers", false, "Prefix currently-connected characters with '*' on the board. Uses data the server already has; costs no extra network traffic.");
		SuppressFloatingText = config.Bind<bool>("Advanced", "SuppressFloatingText", true, "Send server lines from a position far outside the world so clients show them in the chat window only, instead of also spawning floating in-world shout text for each line.");
		ChatCommandsEnabled = config.Bind<bool>("Advanced", "ChatCommandsEnabled", true, "Parse chat commands out of chat that passes through the server's RPC router. NOTE: vanilla only puts a chat message on the wire when a second player is connected, so ! commands need two bodies in the world. Deaths, arrivals and the join board are server-authored and work solo.");
		AnnounceBuildOnJoin = config.Bind<bool>("Advanced", "AnnounceBuildOnJoin", true, "Shout the running Deathboard version and build number as the first line of each server session, so you can tell at a glance from in-game chat whether a new DLL actually took effect. It is also always written to the BepInEx log at startup.");
		SoloCommands = config.Bind<bool>("Advanced", "SoloCommands", false, "Make chat commands work with only one player connected. Vanilla never puts a chat message on the wire when you are alone, so the server has nothing to read. This adds one inert entry to the player list the server broadcasts, addressed to the server itself, which makes clients send it a copy of every chat message. It is designed to be invisible to unmodded clients and does not consume a player slot, but it does change synced vanilla data, so it is off by default. Side effect: the server console logs all player chat while it is on.");
		RealmMode = config.Bind<bool>("Realm", "RealmMode", true, "Master switch for realm characters, and the point of the whole mod: ON (the default) means THIS SERVER OWNS THE CHARACTER. It stores the authoritative copy, hands it back on join, and a character levelled somewhere else cannot be walked in. Players are asked to agree before anything is applied, and their own character file is never read or written. OFF makes this a plain leaderboard server: no character data moves at all. Ignored on a listen server - realm characters require a dedicated server.");
		KickUnmodded = config.Bind<bool>("Realm", "KickUnmodded", true, "Disconnect players who do not answer the realm handshake, i.e. who are not running the client mod. Requires RealmMode, and ON by default because realm mode without it is not actually locked: without it a vanilla client walks straight in with whatever character it likes, which is an item IMPORT channel, not just a bypass - they can dump foreign gear into a chest, reconnect with the mod, and the gear stays in your world. Nothing in vanilla's own handshake can catch them, because Deathboard deliberately does not touch the game or network version. They are disconnected with error 3 (\"Incompatible version\"), the closest of the 13 built-in reasons; no custom message can be sent. If this ever locks you out, edit this file over SFTP and restart - it needs no client.");
		KickUnmoddedGraceSeconds = config.Bind<float>("Realm", "KickUnmoddedGraceSeconds", 15f, new ConfigDescription("How long a player has to complete the realm handshake before KickUnmodded disconnects them. The client greets the server within a second of connecting, well before the character spawns, so this only needs to cover a bad connection.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 120f), Array.Empty<object>()));
		RemoteBoardEnabled = config.Bind<bool>("Realm", "RemoteBoardEnabled", true, "Answer leaderboard requests from players running Deathboard.Client, which is what feeds their in-game ladder window. Deliberately independent of RealmMode: reading a leaderboard should not require handing character ownership to the server. Requests are rate-limited to one per second per player and capped at 200 rows. Unmodded clients are unaffected either way - nothing is registered that vanilla needs to know about, and chat commands keep working as before.");
		AcceptSkillReports = config.Bind<bool>("Realm", "AcceptSkillReports", true, "Accept skill levels reported by players running Deathboard.Client, and use them for the power ratings. This is what puts real numbers on the board: a dedicated server cannot read a player's skills any other way (see README, \"Where ratings come from\"). Only the reporting player's own row is affected, deaths are never taken from a client, and players without the client are unaffected - their rows simply show no rating. Independent of RealmMode.");
		AcceptDeathReports = config.Bind<bool>("Realm", "AcceptDeathReports", true, "Accept \"what killed me\" reports from players running Deathboard.Client, and answer the profile window that shows them. A dedicated server can see THAT a player died but never WHY: the cause lives in Character.m_lastHit, which only ever exists on the dying player's own machine. THE DEATH COUNT ITSELF IS UNAFFECTED - it is measured server-side from the character's ZDO and nothing a client sends can change it, so the worst a modified client can do here is lie about its own cause of death. Independent of RealmMode.");
		DeathCauseInAnnounce = config.Bind<bool>("Realm", "DeathCauseInAnnounce", true, "Put the cause into the global death line, so \"HEINLIEN has died\" becomes \"HEINLIEN was killed by a Troll\". Turn this off to keep the old wording while still collecting causes for the profile window. Does nothing when AcceptDeathReports is off, or for players not running the client mod.");
		DeathAnnounceDelaySeconds = config.Bind<float>("Realm", "DeathAnnounceDelaySeconds", 1.5f, "How long to hold a death announcement waiting for the client to say what killed them. The death is recorded immediately either way; only the chat line waits, and it goes out the instant the cause arrives rather than serving the full delay. 0 disables the wait, which means causes will usually miss the line and only show up in the profile window. Clamped to 10s.");
		DeathCauseWindowSeconds = config.Bind<float>("Realm", "DeathCauseWindowSeconds", 5f, "How long a reported cause stays valid for attaching to a death. Guards against a stale report decorating a later, unrelated death - for example if a player dies on a vanilla client immediately after dying on a modded one.");
		AcceptStats = config.Bind<bool>("Realm", "AcceptStats", true, "Accept activity stats (monsters slain, trees felled, distance travelled, blocks and parries, kills by creature) from players running Deathboard.Client. These fill the stat screen. The client sends every counter it has RAW; this server decides which are shown and what they are called, so re-curating needs no new client. Client-supplied and therefore not proof against a modified client - deaths and the boss row are the numbers that stay authoritative. Independent of RealmMode.");
		ShowBossRow = config.Bind<bool>("Realm", "ShowBossRow", true, "Show the realm boss progress row across the top of the leaderboard window. This is the one part of the board NOBODY can fake: a boss sets a global key in the world save when it dies, and the server reads its own keys. No client is involved.");
		BossOrder = config.Bind<string>("Realm", "BossOrder", "defeated_eikthyr=Eikthyr,defeated_gdking=The Elder,defeated_bonemass=Bonemass,defeated_dragon=Moder,defeated_goblinking=Yagluth,defeated_queen=The Queen,defeated_fader=Fader", "Boss progression order and display names for the boss row, as globalKey=Display Name, comma separated. The server also SCANS the prefabs the game registers for anything marked as a boss, so one added by a game update or a mod still appears - it is appended at the end and logged, because a prefab scan cannot know progression order. Add it here to place it properly.");
		AcceptCharUploads = config.Bind<bool>("Realm", "AcceptCharUploads", true, "Accept full character snapshots from players running Deathboard.Client, and store them under SnapshotDir. Requires RealmMode; with RealmMode off nothing is registered and this does nothing. THIS IS UPLOAD ONLY - the server stores what it is sent and never sends a character back, so no player's character can be altered by turning this on. It gives the server a backup and real power ratings without needing AcceptSkillReports, since skills are read from the snapshot itself. Snapshots are capped at 4 MB and rate-limited per player.");
		SnapshotDir = config.Bind<string>("Realm", "SnapshotDir", "", "Folder holding stored character snapshots, laid out as <steamId>/<name>.fch with a .meta.json beside each. Empty (default) means <BepInEx/config>/Deathboard/characters. Only read when AcceptCharUploads is on.");
		BackupCount = config.Bind<int>("Realm", "BackupCount", 3, "Previous generations kept per character as <name>.fch.bak1 ... bakN, bak1 being the most recent. Clamped to 1..32: a store that can overwrite the only copy of a character is the failure this exists to prevent. An unchanged upload is a no-op and does not consume a backup slot.");
		BackupIntervalMinutes = config.Bind<float>("Realm", "BackupIntervalMinutes", 10f, "How old the newest backup must be before another one is taken. Uploads fire a few seconds after any inventory change, so rotating on every write would leave a 3-deep ring covering half a minute. Pacing it keeps the ring spanning hours and makes the ordinary write one file operation instead of four. The first backup of a character is always taken regardless: nothing is overwritten before a copy of it exists.");
		SkillJumpWarnPoints = config.Bind<float>("Realm", "SkillJumpWarnPoints", 15f, new ConfigDescription("Log a warning when an INDIVIDUAL skill gains more than this many levels between two stored snapshots. Per skill, not per category: a category is a total of up to 13 skills, so its delta cannot tell an hour of honest play apart from one skill leaping 70 levels in a minute. LOG ONLY - nothing is rejected and no one is kicked. Skills rise slowly in normal play, so a large step between two uploads minutes apart is worth an admin's eye. Deliberately advisory: the value of this design is the server owning the snapshot, not delta thresholds, and a false positive that bounced a real player would cost more than it saved. 0 disables it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>()));
		Format = config.Bind<string>("Power", "Format", "{name} — {deaths}   {P} ({C},{A},{H})", "Row layout. Tokens: {name} {deaths} {P} {C} {A} {H}. P is the composite rating, C/A/H are the category averages (0-100, printed to the nearest whole number, halves rounded away from zero).");
		PowerWeightsRaw = config.Bind<string>("Power", "PowerWeights", "0.50, 0.25, 0.25", "Weights for Combat, Acro and Hand in P = wC*C + wA*A + wH*H. With the defaults a character at 100 in everything rates 100.");
		BoardTitle = config.Bind<string>("General", "BoardTitle", "The Great Hall", "What the board calls itself, in the client's window and at the top of a chat dump. Other boards append what they are sorted by, e.g. \"The Great Hall (most deaths)\". \"Deathboard Leaderboard\" said the same thing twice; anything short works - The Great Hall, Hall of the Slain, The Reckoning, or just Deathboard.");
		Labels = config.Bind<string>("Power", "Labels", "Combat,Agility,Trades", "Column labels for the three categories, used in the board legend and headers.");
		ShowPowerOnName = config.Bind<bool>("Power", "ShowPowerOnName", true, "Show the rating block on roster, death and arrival lines. When false, Format is trimmed at the first {P} token, so the default becomes just \"{name} — {deaths}\".");
		SkillRefreshSeconds = config.Bind<float>("Power", "SkillRefreshSeconds", 10f, new ConfigDescription("How often connected characters' skills are re-read. Reads are cheap and are skipped entirely when no source is available.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 300f), Array.Empty<object>()));
		SkillsSource = config.Bind<SkillsSourceMode>("Power", "SkillsSource", SkillsSourceMode.Auto, "Where ratings come from. Auto: live server-side Player object if one exists, otherwise .fch character files. PlayerObject / CharacterFiles: force one. Off: never read skills, every row prints '?'. IMPORTANT: a vanilla dedicated server cannot see a remote player's skills at all - see README.md, \"Where ratings come from\" - so on a stock setup this will stay '?' until you give it character files.");
		CharacterFilesPath = config.Bind<string>("Power", "CharacterFilesPath", "", "Folder searched recursively for *.fch character saves. Empty tries <BepInEx/config>/Deathboard/characters and then <BepInEx/config>/ServerCharacters/Characters. Files are matched to a character by filename: \"Bjorn.fch\", or \"<steamid>_Bjorn.fch\" / \"Bjorn_<something>.fch\".");
	}

	public PowerWeights Weights(Action<string> logWarning)
	{
		string error;
		PowerWeights result = PowerWeights.Parse(PowerWeightsRaw.Value, out error);
		if (error != null)
		{
			logWarning?.Invoke("PowerWeights: " + error + "; using 0.50, 0.25, 0.25.");
		}
		return result;
	}

	public BoardOptions MakeBoardOptions(Action<string> logWarning)
	{
		return new BoardOptions
		{
			Format = Format.Value,
			Labels = PowerRating.ParseLabels(Labels.Value),
			Weights = Weights(logWarning),
			ShowPower = ShowPowerOnName.Value,
			MarkOnline = MarkOnlinePlayers.Value,
			LinesPerMessage = LinesPerShout.Value,
			Title = BoardTitle.Value
		};
	}

	public IEnumerable<string> DefaultCharacterDirectories()
	{
		yield return Path.Combine(Path.Combine(Paths.ConfigPath, "Deathboard"), "characters");
		yield return Path.Combine(Path.Combine(Paths.ConfigPath, "ServerCharacters"), "Characters");
	}
}
[BepInPlugin("com.deathboard.valheim", "The Great Hall", "3.0.0")]
public sealed class DeathboardPlugin : BaseUnityPlugin
{
	private sealed class PendingDeath
	{
		public PeerCharacter Character;

		public DeathRecord Record;

		public float AnnounceAt;
	}

	private sealed class BufferedCause
	{
		public string Key;

		public string Display;

		public float At;
	}

	public const string PluginGuid = "com.deathboard.valheim";

	public const string PluginName = "Deathboard";

	public const string PluginVersion = "3.0.0";

	private DeathboardConfig _config;

	private DeathLedger _ledger;

	private DeathWatch _watch;

	private Announcer _announcer;

	private DiscordWebhook _discord;

	private SkillSource _skills;

	private RealmServer _realm;

	private SnapshotStore _snapshots;

	private Harmony _harmony;

	private CauseStore _causes;

	private StatsStore _stats;

	private BossRoster _bosses;

	private float _pollTimer;

	private float _skillTimer;

	private float _rebroadcastTimer;

	private bool _buildAnnounced;

	private readonly Dictionary<string, PendingDeath> _pendingDeaths = new Dictionary<string, PendingDeath>(StringComparer.Ordinal);

	private readonly Dictionary<string, BufferedCause> _causeBuffer = new Dictionary<string, BufferedCause>(StringComparer.Ordinal);

	private readonly Dictionary<string, float> _lastJoinAnnounce = new Dictionary<string, float>(StringComparer.Ordinal);

	private readonly Dictionary<string, float> _recentChat = new Dictionary<string, float>(StringComparer.Ordinal);

	private const float ChatDedupeSeconds = 3f;

	internal static DeathboardPlugin Instance { get; private set; }

	internal bool SoloCommandsEnabled
	{
		get
		{
			if (_config != null && _config.ChatCommandsEnabled.Value)
			{
				return _config.SoloCommands.Value;
			}
			return false;
		}
	}

	internal string ListenerName
	{
		get
		{
			string text = ((_config != null) ? _config.AnnouncementName.Value : null);
			if (!string.IsNullOrEmpty(text))
			{
				return text;
			}
			return "Deathboard";
		}
	}

	private void Awake()
	{
		//IL_0303: Unknown result type (might be due to invalid IL or missing references)
		//IL_030d: Expected O, but got Unknown
		Instance = this;
		LogInfo("===== " + BuildInfo.ServerBanner + " starting =====");
		PatchGuard.Log = LogError;
		_config = new DeathboardConfig(((BaseUnityPlugin)this).Config);
		string path = Path.Combine(Paths.ConfigPath, "Deathboard");
		_ledger = new DeathLedger(Path.Combine(path, "deaths.json"), LogInfo, LogError);
		_ledger.Load();
		_skills = new SkillSource(_config, LogInfo, LogWarn);
		string text = _config.SnapshotDir.Value;
		if (string.IsNullOrEmpty(text))
		{
			text = Path.Combine(path, "characters");
		}
		_snapshots = new SnapshotStore(text, _config.BackupCount.Value, _config.BackupIntervalMinutes.Value, LogInfo, LogError);
		_causes = new CauseStore(Path.Combine(path, "causes.json"), LogInfo, LogError);
		_causes.Load();
		_stats = new StatsStore(Path.Combine(path, "stats.json"), LogInfo, LogError);
		_stats.Load();
		_bosses = new BossRoster(_config, LogInfo, LogWarn);
		_realm = new RealmServer(_config, BuildBoardPacket, ApplyReportedSkills, ApplyCharUpload, LoadStoredCharacter, ApplyDeathReport, BuildProfilePacket, ApplyStatsReport, LogInfo, LogWarn);
		_watch = new DeathWatch(_ledger, _skills, Weights, LogInfo, LogWarn);
		_announcer = new Announcer(_config, _watch, LogWarn);
		_discord = new DiscordWebhook(() => _config.DiscordWebhookUrl.Value, LogWarn);
		_watch.CharacterJoined += OnCharacterJoined;
		_watch.CharacterDied += OnCharacterDied;
		_watch.CharacterLeft += delegate(PeerCharacter c)
		{
			_realm.OnPeerDisconnected(c.PeerId);
			if (_stats != null)
			{
				_stats.EndSession(c.SteamId, c.Name);
			}
		};
		_harmony = new Harmony("com.deathboard.valheim");
		_harmony.PatchAll(Assembly.GetExecutingAssembly());
		LogInfo(BuildInfo.ServerBanner + " loaded. SoloCommands=" + _config.SoloCommands.Value + " RealmMode=" + _config.RealmMode.Value + " RemoteBoard=" + _config.RemoteBoardEnabled.Value + " ChatType=" + _config.ChatType.Value + " SkillsSource=" + _config.SkillsSource.Value.ToString() + " Ledger: " + _ledger.Path);
	}

	private void OnDestroy()
	{
		if (_harmony != null)
		{
			_harmony.UnpatchSelf();
		}
		Instance = null;
	}

	private void Update()
	{
		if (!DeathWatch.ServerActive())
		{
			return;
		}
		_realm.EnsureRegistered();
		_realm.Tick();
		_skillTimer += Time.deltaTime;
		if (_skillTimer >= _config.SkillRefreshSeconds.Value)
		{
			_skillTimer = 0f;
			try
			{
				_watch.RefreshSkills();
			}
			catch (Exception ex)
			{
				LogError("Skill refresh failed: " + ex);
			}
		}
		TickRebroadcast();
		TickPendingDeaths();
		_pollTimer += Time.deltaTime;
		if (_pollTimer < _config.PollIntervalSeconds.Value)
		{
			return;
		}
		_pollTimer = 0f;
		try
		{
			_watch.Tick();
		}
		catch (Exception ex2)
		{
			LogError("Peer poll failed: " + ex2);
		}
	}

	private void TickRebroadcast()
	{
		float value = _config.RebroadcastSeconds.Value;
		if (value <= 0f)
		{
			_rebroadcastTimer = 0f;
			return;
		}
		_rebroadcastTimer += Time.deltaTime;
		if (_rebroadcastTimer < value)
		{
			return;
		}
		_rebroadcastTimer = 0f;
		if (!AnyoneOnline())
		{
			return;
		}
		try
		{
			SendBoard(null, BoardKind.Deaths, BoardSort.Ascending);
		}
		catch (Exception ex)
		{
			LogError("Rebroadcast failed: " + ex);
		}
	}

	private bool AnyoneOnline()
	{
		using (IEnumerator<PeerCharacter> enumerator = _watch.Online.GetEnumerator())
		{
			if (enumerator.MoveNext())
			{
				_ = enumerator.Current;
				return true;
			}
		}
		return false;
	}

	private PowerWeights Weights()
	{
		return _config.Weights(LogWarn);
	}

	private void LogInfo(string message)
	{
		((BaseUnityPlugin)this).Logger.LogInfo((object)("[Deathboard] " + message));
	}

	private void LogWarn(string message)
	{
		((BaseUnityPlugin)this).Logger.LogWarning((object)("[Deathboard] " + message));
	}

	private void LogError(string message)
	{
		((BaseUnityPlugin)this).Logger.LogError((object)("[Deathboard] " + message));
	}

	private void OnCharacterJoined(PeerCharacter character, DeathRecord record, bool isNewCharacter)
	{
		if (ShouldAnnounceJoin(character.LedgerKey))
		{
			AnnounceBuildOnce(character);
			if (_config.ArriveAnnounce.Value)
			{
				_announcer.Say(character, _announcer.Tag(Board.SafeName(character.Name) + " has arrived (" + record.Deaths + " deaths)" + StatsSuffix(record)));
			}
			string value = _config.ArriveHint.Value;
			if (!string.IsNullOrEmpty(value))
			{
				_announcer.SayTo(character, _announcer.Tag(value));
			}
			if (_config.PrintBoardOnArrive.Value)
			{
				SendBoard(character, BoardKind.Deaths, BoardSort.Ascending);
			}
			_discord.Post(character.Name + " joined" + (isNewCharacter ? " for the first time" : "") + " (" + record.Deaths + " deaths)" + StatsSuffix(record) + ".");
		}
	}

	private void OnCharacterDied(PeerCharacter character, DeathRecord record)
	{
		if (_config.DeathAnnounce.Value || !string.IsNullOrEmpty(_config.DiscordWebhookUrl.Value))
		{
			float num = Mathf.Clamp(_config.DeathAnnounceDelaySeconds.Value, 0f, 10f);
			if (num <= 0f || HasFreshCause(character))
			{
				AnnounceDeath(character, record);
			}
			else
			{
				_pendingDeaths[character.LedgerKey] = new PendingDeath
				{
					Character = character,
					Record = record,
					AnnounceAt = Time.realtimeSinceStartup + num
				};
			}
		}
		if (_config.RebroadcastOnDeath.Value)
		{
			SendBoard(character, BoardKind.Deaths, BoardSort.Ascending);
			_rebroadcastTimer = 0f;
		}
	}

	private void TickPendingDeaths()
	{
		if (_pendingDeaths.Count == 0)
		{
			return;
		}
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		List<string> list = null;
		foreach (KeyValuePair<string, PendingDeath> pendingDeath2 in _pendingDeaths)
		{
			if (!(realtimeSinceStartup < pendingDeath2.Value.AnnounceAt))
			{
				if (list == null)
				{
					list = new List<string>();
				}
				list.Add(pendingDeath2.Key);
			}
		}
		if (list != null)
		{
			for (int i = 0; i < list.Count; i++)
			{
				PendingDeath pendingDeath = _pendingDeaths[list[i]];
				_pendingDeaths.Remove(list[i]);
				AnnounceDeath(pendingDeath.Character, pendingDeath.Record);
			}
		}
	}

	private void AnnounceDeath(PeerCharacter character, DeathRecord record)
	{
		if (character != null && record != null)
		{
			string text = ((record.Deaths == 1) ? "death" : "deaths");
			string text2 = Board.SafeName(character.Name);
			string text3 = text2 + " has died";
			if (_config.DeathCauseInAnnounce.Value && TryTakeCause(character, out var key, out var display))
			{
				text3 = DeathCause.Sentence(text2, key, display);
			}
			string text4 = text3 + " (" + record.Deaths + " " + text + ")" + StatsSuffix(record);
			if (_config.DeathAnnounce.Value)
			{
				_announcer.Say(character, _announcer.Tag(text4), _config.DeathChatType.Value);
			}
			_discord.Post(text4 + ".");
		}
	}

	private bool HasFreshCause(PeerCharacter character)
	{
		if (character == null)
		{
			return false;
		}
		if (!_causeBuffer.TryGetValue(character.LedgerKey, out var value))
		{
			return false;
		}
		return Time.realtimeSinceStartup - value.At <= Mathf.Max(0f, _config.DeathCauseWindowSeconds.Value);
	}

	private bool TryTakeCause(PeerCharacter character, out string key, out string display)
	{
		key = null;
		display = null;
		if (character == null)
		{
			return false;
		}
		string ledgerKey = character.LedgerKey;
		if (!_causeBuffer.TryGetValue(ledgerKey, out var value))
		{
			return false;
		}
		_causeBuffer.Remove(ledgerKey);
		if (Time.realtimeSinceStartup - value.At > Mathf.Max(0f, _config.DeathCauseWindowSeconds.Value))
		{
			return false;
		}
		key = value.Key;
		display = value.Display;
		return true;
	}

	private bool ApplyDeathReport(long senderPeerId, RealmProtocol.DeathReport report)
	{
		if (!TryResolvePeer(senderPeerId, out var steamId, out var characterName))
		{
			LogWarn("Death report from peer " + senderPeerId + " could not be attributed to a connected peer; ignoring.");
			return false;
		}
		string text = DeathCause.KeyFor(report.HitType, report.AttackerKey, report.AttackerIsPlayer);
		if (string.IsNullOrEmpty(text))
		{
			return false;
		}
		string attackerDisplay = report.AttackerDisplay;
		string key = DeathLedger.MakeKey(steamId, characterName);
		_causeBuffer[key] = new BufferedCause
		{
			Key = text,
			Display = attackerDisplay,
			At = Time.realtimeSinceStartup
		};
		int worldDay = 0;
		EnvMan instance = EnvMan.instance;
		if ((Object)(object)instance != (Object)null)
		{
			worldDay = instance.GetDay();
		}
		if (_causes.Record(steamId, characterName, text, attackerDisplay, worldDay))
		{
			_causes.Save();
			LogInfo(characterName + " killed by " + DeathCause.ShortLabel(text, attackerDisplay) + " (" + DeathCause.Category(text) + ").");
		}
		else
		{
			LogWarn(characterName + " has hit the per-character cause limit (" + 256 + "); dropping cause '" + text + "'.");
		}
		if (_pendingDeaths.TryGetValue(key, out var value))
		{
			_pendingDeaths.Remove(key);
			AnnounceDeath(value.Character, value.Record);
		}
		return true;
	}

	private ZPackage BuildProfilePacket(string name)
	{
		ProfileView profileView = new ProfileView
		{
			Name = BoardPayload.Trim(name)
		};
		BoardOptions boardOptions = _config.MakeBoardOptions(LogWarn);
		profileView.Labels = boardOptions.Labels;
		DeathRecord deathRecord = FindLedgerRow(profileView.Name);
		CauseRecord record3;
		if (deathRecord != null)
		{
			profileView.Found = true;
			profileView.Deaths = deathRecord.Deaths;
			profileView.Online = _watch.IsOnline(deathRecord.SteamId, deathRecord.Name);
			profileView.FirstSeenUtc = deathRecord.FirstSeenUtc;
			profileView.LastSeenUtc = deathRecord.LastSeenUtc;
			profileView.HasSkills = deathRecord.HasSkills;
			if (deathRecord.HasSkills)
			{
				profileView.Combat = deathRecord.Combat;
				profileView.Acro = deathRecord.Acro;
				profileView.Hand = deathRecord.Hand;
				profileView.Power = PowerRating.Compute(deathRecord.Skills, Weights());
			}
			if (_causes.TryGet(deathRecord.SteamId, deathRecord.Name, out var record))
			{
				FillCauses(profileView, record);
			}
			if (_stats.TryGet(deathRecord.SteamId, deathRecord.Name, out var record2))
			{
				FillStats(profileView, record2);
			}
		}
		else if (_causes.TryGetByName(profileView.Name, out record3))
		{
			profileView.Found = true;
			FillCauses(profileView, record3);
			if (_stats.TryGetByName(profileView.Name, out var record4))
			{
				FillStats(profileView, record4);
			}
		}
		return ProfilePayload.WriteProfile(profileView);
	}

	private static void FillStats(ProfileView view, StatsRecord stats)
	{
		foreach (StatDef item in StatCatalog.Definitions())
		{
			if (view.Stats.Count >= 128)
			{
				break;
			}
			float num = stats.Get(item.Key);
			if (item.Group != StatGroup.Death || !(num <= 0f))
			{
				view.Stats.Add(new ProfileStat
				{
					Key = item.Key,
					Label = item.Label,
					Group = (byte)item.Group,
					Value = StatCatalog.Format(item, num),
					Raw = num
				});
			}
		}
		FillSkills(view, stats);
		FillEquipment(view, stats);
		List<KeyValuePair<string, float>> list = stats.EnemiesSorted();
		for (int i = 0; i < list.Count && i < 256; i++)
		{
			view.Kills.Add(new ProfileCause
			{
				Key = list[i].Key,
				Display = StatCatalog.PrettyToken(list[i].Key),
				Count = (int)list[i].Value
			});
		}
	}

	private static void FillSkills(ProfileView view, StatsRecord stats)
	{
		List<int> list = new List<int>(stats.Skills.Keys);
		list.Sort();
		for (int i = 0; i < list.Count; i++)
		{
			if (view.Skills.Count >= 64)
			{
				break;
			}
			int num = list[i];
			float num2 = stats.Skills[num];
			if (!(num2 <= 0f) && SkillBuckets.IsRealSkillId(num))
			{
				view.Skills.Add(new ProfileSkill
				{
					Id = num,
					Label = SkillLabel(num),
					Group = SkillGroup(num),
					Level = num2
				});
			}
		}
	}

	private static string SkillLabel(int id)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		string text;
		try
		{
			text = ((object)(SkillType)id/*cast due to .constrained prefix*/).ToString();
		}
		catch (Exception)
		{
			return "Skill " + id;
		}
		if (string.IsNullOrEmpty(text))
		{
			return "Skill " + id;
		}
		StringBuilder stringBuilder = new StringBuilder(text.Length + 4);
		for (int i = 0; i < text.Length; i++)
		{
			if (i > 0 && char.IsUpper(text[i]) && !char.IsUpper(text[i - 1]))
			{
				stringBuilder.Append(' ');
			}
			stringBuilder.Append(text[i]);
		}
		return stringBuilder.ToString();
	}

	private static byte SkillGroup(int id)
	{
		for (int i = 0; i < SkillBuckets.CombatIds.Length; i++)
		{
			if (SkillBuckets.CombatIds[i] == id)
			{
				return 0;
			}
		}
		for (int j = 0; j < SkillBuckets.AthleticsIds.Length; j++)
		{
			if (SkillBuckets.AthleticsIds[j] == id)
			{
				return 1;
			}
		}
		return 2;
	}

	private static void FillEquipment(ProfileView view, StatsRecord stats)
	{
		for (int i = 0; i < stats.Equipment.Count; i++)
		{
			if (view.Equipment.Count >= 24)
			{
				break;
			}
			string[] array = stats.Equipment[i].Split(new char[1] { ':' });
			if (array.Length == 3 && array[0].Length != 0 && byte.TryParse(array[1], out var result))
			{
				if (!byte.TryParse(array[2], out var result2))
				{
					result2 = 0;
				}
				view.Equipment.Add(new ProfileEquip
				{
					Prefab = array[0],
					Slot = result,
					Quality = result2
				});
			}
		}
	}

	private bool ApplyStatsReport(long senderPeerId, RealmProtocol.StatsReport report)
	{
		if (!TryResolvePeer(senderPeerId, out var steamId, out var characterName))
		{
			LogWarn("Stats report from peer " + senderPeerId + " could not be attributed to a connected peer; ignoring.");
			return false;
		}
		Dictionary<string, float> dictionary = new Dictionary<string, float>(StringComparer.Ordinal);
		if (report.Counters != null)
		{
			foreach (KeyValuePair<int, float> counter in report.Counters)
			{
				if (StatCatalog.TryByOrdinal(counter.Key, out var def))
				{
					dictionary[def.Key] = counter.Value;
				}
			}
		}
		if (report.Mod != null)
		{
			foreach (KeyValuePair<string, float> item in report.Mod)
			{
				if (StatCatalog.TryByKey(item.Key, out var def2))
				{
					dictionary[def2.Key] = item.Value;
				}
			}
		}
		bool flag = _stats.Apply(steamId, characterName, report.SessionNonce, dictionary, report.Enemies);
		List<string> list = new List<string>();
		if (report.Equipment != null)
		{
			for (int i = 0; i < report.Equipment.Count; i++)
			{
				RealmProtocol.EquippedItem equippedItem = report.Equipment[i];
				list.Add(equippedItem.Prefab + ":" + equippedItem.Slot + ":" + equippedItem.Quality);
			}
		}
		if (!(flag | _stats.SetEquipment(steamId, characterName, list)))
		{
			return false;
		}
		_stats.Save();
		return true;
	}

	private static void FillCauses(ProfileView view, CauseRecord causes)
	{
		view.Attributed = causes.Attributed;
		List<CauseTally> list = causes.Sorted();
		for (int i = 0; i < list.Count && i < 256; i++)
		{
			view.Causes.Add(new ProfileCause
			{
				Key = list[i].Key,
				Display = list[i].Display,
				Count = list[i].Count
			});
		}
		for (int j = 0; j < causes.Recent.Count && j < 64; j++)
		{
			RecentDeath recentDeath = causes.Recent[j];
			view.Recent.Add(new ProfileDeath
			{
				Utc = recentDeath.Utc,
				Day = recentDeath.Day,
				Key = recentDeath.Key,
				Display = recentDeath.Display
			});
		}
	}

	private DeathRecord FindLedgerRow(string name)
	{
		if (string.IsNullOrEmpty(name))
		{
			return null;
		}
		DeathRecord deathRecord = null;
		List<DeathRecord> list = _ledger.Snapshot();
		for (int i = 0; i < list.Count; i++)
		{
			DeathRecord deathRecord2 = list[i];
			if (string.Equals(deathRecord2.Name, name, StringComparison.Ordinal))
			{
				if (_watch.IsOnline(deathRecord2.SteamId, deathRecord2.Name))
				{
					return deathRecord2;
				}
				if (deathRecord == null || string.CompareOrdinal(deathRecord2.LastSeenUtc ?? "", deathRecord.LastSeenUtc ?? "") > 0)
				{
					deathRecord = deathRecord2;
				}
			}
		}
		return deathRecord;
	}

	private string StatsSuffix(DeathRecord record)
	{
		if (!_config.ShowPowerOnName.Value)
		{
			return "";
		}
		string text = PowerRating.StatsBlock(record.Skills, Weights());
		if (text.Length != 0)
		{
			return "   " + text;
		}
		return "";
	}

	private void AnnounceBuildOnce(PeerCharacter to)
	{
		if (!_buildAnnounced && _config.AnnounceBuildOnJoin.Value)
		{
			_buildAnnounced = true;
			_announcer.Say(to, _announcer.Tag(BuildInfo.PublicBanner));
		}
	}

	private bool ShouldAnnounceJoin(string ledgerKey)
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (_lastJoinAnnounce.TryGetValue(ledgerKey, out var value) && realtimeSinceStartup - value < _config.JoinDebounceSeconds.Value)
		{
			return false;
		}
		_lastJoinAnnounce[ledgerKey] = realtimeSinceStartup;
		return true;
	}

	private void SendBoard(PeerCharacter attributeTo, BoardKind kind, BoardSort sort)
	{
		Func<DeathRecord, bool> isOnline = (DeathRecord r) => _watch.IsOnline(r.SteamId, r.Name);
		List<string> list = Board.Render(_ledger.Snapshot(), kind, sort, _config.MakeBoardOptions(LogWarn), isOnline);
		for (int num = 0; num < 1 && num < list.Count; num++)
		{
			list[num] = _announcer.Tag(list[num]);
		}
		_announcer.Say(attributeTo, list);
	}

	private static bool TryResolvePeer(long senderPeerId, out string steamId, out string characterName)
	{
		steamId = null;
		characterName = null;
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance != (Object)null)
		{
			ZNetPeer peer = instance.GetPeer(senderPeerId);
			if (peer != null && peer.m_socket != null)
			{
				steamId = peer.m_socket.GetHostName();
				characterName = peer.m_playerName;
			}
		}
		if (!string.IsNullOrEmpty(steamId))
		{
			return !string.IsNullOrEmpty(characterName);
		}
		return false;
	}

	private bool ApplyCharUpload(long senderPeerId, RealmProtocol.CharUpload upload)
	{
		if (_snapshots == null || upload.Blob == null || upload.Blob.Length == 0)
		{
			return false;
		}
		if (!TryResolvePeer(senderPeerId, out var steamId, out var characterName))
		{
			LogWarn("Snapshot from peer " + senderPeerId + " could not be attributed to a connected peer; ignoring. Nothing stored.");
			return false;
		}
		Dictionary<int, float> before = null;
		string previousStamp = null;
		if (_snapshots.TryLoad(steamId, characterName, out var blob, out var meta))
		{
			before = FchSkillReader.FindSkillBlock(blob, SkillBuckets.IsRealSkillId);
			previousStamp = meta?.UpdatedUtc;
		}
		SnapshotMeta meta2;
		switch (_snapshots.Save(steamId, characterName, upload.Blob, out meta2))
		{
		case SnapshotSaveResult.Rejected:
			LogWarn("Rejected a " + upload.Blob.Length + "-byte snapshot for " + characterName + " (" + steamId + "); any stored copy is unchanged.");
			return false;
		case SnapshotSaveResult.Written:
			LogInfo("Stored snapshot generation " + meta2.Generation + " for " + characterName + " (" + steamId + "), " + meta2.ByteCount + " bytes, reason=" + upload.Reason.ToString() + ".");
			break;
		}
		if (upload.World != null && upload.World.Length != 0)
		{
			_snapshots.SaveWorld(steamId, characterName, upload.World);
		}
		return RefreshRatingFromBlob(steamId, characterName, upload.Blob, before, previousStamp);
	}

	private RealmServer.StoredCharacter LoadStoredCharacter(long senderPeerId)
	{
		RealmServer.StoredCharacter result = default(RealmServer.StoredCharacter);
		if (_snapshots == null)
		{
			return result;
		}
		if (!TryResolvePeer(senderPeerId, out var steamId, out var characterName))
		{
			LogWarn("Cannot look up a character for peer " + senderPeerId + ": not attributable to a connected peer. Sending no handover.");
			return result;
		}
		if (!_snapshots.TryLoad(steamId, characterName, out var blob, out var meta))
		{
			LogInfo("No stored character for " + characterName + " (" + steamId + ") - first join here.");
			return result;
		}
		_snapshots.TryLoadWorld(steamId, characterName, out var world);
		return new RealmServer.StoredCharacter
		{
			Found = true,
			Generation = meta.Generation,
			Blob = blob,
			World = world
		};
	}

	private bool RefreshRatingFromBlob(string steamId, string characterName, byte[] blob, Dictionary<int, float> before, string previousStamp)
	{
		Dictionary<int, float> dictionary = FchSkillReader.FindSkillBlock(blob, SkillBuckets.IsRealSkillId);
		if (dictionary == null || dictionary.Count == 0)
		{
			LogWarn("No skill block found in " + characterName + "'s snapshot; it is stored, but the rating is unchanged. This is expected if the .fch layout moved in a game patch.");
			return false;
		}
		WarnOnSkillJump(characterName, before, dictionary, previousStamp);
		SkillProfile profile = SkillBuckets.Profile(dictionary);
		if (!profile.HasData)
		{
			return false;
		}
		if (!_ledger.TryGet(steamId, characterName, out var record))
		{
			_ledger.EnsureCharacter(steamId, characterName, out record);
		}
		if (!_ledger.UpdateSkills(record, profile, Weights()))
		{
			return false;
		}
		_ledger.Save();
		LogInfo(characterName + " rating updated from stored snapshot: " + PowerRating.StatsBlock(record.Skills, Weights()));
		return true;
	}

	private void WarnOnSkillJump(string characterName, Dictionary<int, float> before, Dictionary<int, float> after, string previousStamp)
	{
		float value = _config.SkillJumpWarnPoints.Value;
		if (value <= 0f || before == null || after == null)
		{
			return;
		}
		string text = null;
		float num = 0f;
		float num2 = 0f;
		float num3 = 0f;
		foreach (KeyValuePair<int, float> item in after)
		{
			float value2 = 0f;
			before.TryGetValue(item.Key, out value2);
			float num4 = item.Value - value2;
			if (!(num4 <= num))
			{
				num = num4;
				text = SkillName(item.Key);
				num2 = value2;
				num3 = item.Value;
			}
		}
		if (text != null && !(num <= value))
		{
			LogWarn("Skill jump: " + characterName + "'s " + text + " went " + num2.ToString("0.#") + " -> " + num3.ToString("0.#") + " (+" + num.ToString("0.#") + ") since " + (string.IsNullOrEmpty(previousStamp) ? "the previous snapshot" : previousStamp) + ", over the " + value + "-point warning threshold. Nothing has been rejected and nobody has been kicked - this is advisory only.");
		}
	}

	private static string SkillName(int id)
	{
		string name = Enum.GetName(typeof(SkillType), id);
		if (!string.IsNullOrEmpty(name))
		{
			return name;
		}
		return "skill " + id;
	}

	private bool ApplyReportedSkills(long senderPeerId, Dictionary<int, float> levels)
	{
		if (levels == null || levels.Count == 0)
		{
			return false;
		}
		if (!TryResolvePeer(senderPeerId, out var steamId, out var characterName))
		{
			LogWarn("Skill report from peer " + senderPeerId + " could not be attributed to a connected peer; ignoring.");
			return false;
		}
		SkillProfile profile = SkillBuckets.Profile(levels);
		if (!profile.HasData)
		{
			return false;
		}
		if (!_ledger.TryGet(steamId, characterName, out var record))
		{
			_ledger.EnsureCharacter(steamId, characterName, out record);
		}
		if (_stats != null && _stats.SetSkills(steamId, characterName, levels))
		{
			_stats.Save();
		}
		if (!_ledger.UpdateSkills(record, profile, Weights()))
		{
			return false;
		}
		_ledger.Save();
		LogInfo(characterName + " rating updated from client report: " + PowerRating.StatsBlock(record.Skills, Weights()));
		return true;
	}

	private ZPackage BuildBoardPacket(byte wireKind, byte wireSort)
	{
		BoardKind kind = (BoardKind)((wireKind < 6) ? wireKind : 0);
		BoardSort boardSort = ((wireSort == 1) ? BoardSort.Descending : BoardSort.Ascending);
		BoardOptions boardOptions = _config.MakeBoardOptions(LogWarn);
		Func<DeathRecord, bool> func = (DeathRecord r) => _watch.IsOnline(r.SteamId, r.Name);
		List<DeathRecord> list = _ledger.Snapshot();
		int num = 0;
		for (int num2 = 0; num2 < list.Count; num2++)
		{
			num += list[num2].Deaths;
		}
		int worldDay = 0;
		EnvMan instance = EnvMan.instance;
		if ((Object)(object)instance != (Object)null)
		{
			worldDay = instance.GetDay();
		}
		List<DeathRecord> list2 = Board.Select(list, kind, boardSort, boardOptions.Weights, func);
		List<BoardRow> list3 = new List<BoardRow>(list2.Count);
		for (int num3 = 0; num3 < list2.Count; num3++)
		{
			DeathRecord deathRecord = list2[num3];
			SkillProfile skills = deathRecord.Skills;
			bool hasSkills = PowerRating.HasVisibleRating(skills, boardOptions.Weights);
			list3.Add(new BoardRow
			{
				Name = deathRecord.Name,
				Deaths = deathRecord.Deaths,
				HasSkills = hasSkills,
				Combat = skills.Combat,
				Acro = skills.Acro,
				Hand = skills.Hand,
				Power = PowerRating.Compute(skills, boardOptions.Weights),
				Online = func(deathRecord)
			});
		}
		return BoardPayload.WriteBoard(wireKind, (boardSort == BoardSort.Descending) ? ((byte)1) : ((byte)0), Board.Header(kind, boardSort, boardOptions.Labels, boardOptions.Title), boardOptions.Labels, boardOptions.ShowPower, boardOptions.ShowPower ? PowerRating.FormulaText(boardOptions.Weights, boardOptions.Labels) : "", num, worldDay, list3, (_bosses == null) ? null : _bosses.Build());
	}

	internal void OnRoutedRpc(ZPackage pkg)
	{
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_002f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0067: Unknown result type (might be due to invalid IL or missing references)
		if (pkg == null || !DeathWatch.ServerActive())
		{
			return;
		}
		ZPackage val = null;
		long senderPeerId;
		ZDOID targetZdo;
		int num;
		try
		{
			pkg.SetPos(0);
			pkg.ReadLong();
			senderPeerId = pkg.ReadLong();
			pkg.ReadLong();
			targetZdo = pkg.ReadZDOID();
			num = pkg.ReadInt();
			if (num == VanillaChat.ChatMessageRpcHash || num == VanillaChat.SayRpcHash)
			{
				val = pkg.ReadPackage();
			}
		}
		catch (Exception)
		{
			return;
		}
		try
		{
			VanillaChat.IncomingChat chat;
			if (num == DeathWatch.OnDeathRpcHash)
			{
				_watch.NoteOnDeathRpc(targetZdo);
			}
			else if (val != null && _config.ChatCommandsEnabled.Value && ((num == VanillaChat.ChatMessageRpcHash) ? VanillaChat.TryReadChatMessage(val, senderPeerId, out chat) : VanillaChat.TryReadSay(val, senderPeerId, out chat)))
			{
				HandleChat(chat);
			}
		}
		catch (Exception ex2)
		{
			LogError("Routed RPC handling failed: " + ex2);
		}
	}

	private void HandleChat(VanillaChat.IncomingChat chat)
	{
		if (string.IsNullOrEmpty(chat.Text) || chat.TalkType == 3)
		{
			return;
		}
		string value = _config.CommandPrefix.Value;
		if (string.IsNullOrEmpty(value))
		{
			return;
		}
		string text = chat.Text.Trim();
		if (!text.StartsWith(value, StringComparison.Ordinal) || !IsFreshChat(chat.SenderPeerId, text))
		{
			return;
		}
		string[] array = text.Substring(value.Length).Split(new char[2] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
		if (array.Length == 0 || !TryParseBoardKind(array[0], out var kind))
		{
			return;
		}
		_watch.TryGetOnline(chat.SenderPeerId, out var character);
		BoardSort sort = Board.DefaultSort(kind);
		if (array.Length > 2)
		{
			_announcer.Say(character, _announcer.Tag(Board.Usage(value)));
			return;
		}
		if (array.Length == 2)
		{
			string text2 = array[1].ToLowerInvariant();
			if (text2 == "asc")
			{
				sort = BoardSort.Ascending;
			}
			else
			{
				if (!(text2 == "desc"))
				{
					_announcer.Say(character, _announcer.Tag(Board.Usage(value)));
					return;
				}
				sort = BoardSort.Descending;
			}
		}
		LogInfo(((character != null) ? character.Name : ("peer " + chat.SenderPeerId)) + " ran " + text);
		SendBoard(character, kind, sort);
	}

	private static bool TryParseBoardKind(string word, out BoardKind kind)
	{
		switch (word.ToLowerInvariant())
		{
		case "deathboard":
		case "vikings":
			kind = BoardKind.Deaths;
			return true;
		case "power":
			kind = BoardKind.Power;
			return true;
		case "com":
		case "combat":
			kind = BoardKind.Combat;
			return true;
		case "acro":
			kind = BoardKind.Acro;
			return true;
		case "hand":
			kind = BoardKind.Hand;
			return true;
		case "who":
			kind = BoardKind.Who;
			return true;
		default:
			kind = BoardKind.Deaths;
			return false;
		}
	}

	private bool IsFreshChat(long senderPeerId, string text)
	{
		float realtimeSinceStartup = Time.realtimeSinceStartup;
		if (_recentChat.Count > 64)
		{
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, float> item in _recentChat)
			{
				if (realtimeSinceStartup - item.Value > 3f)
				{
					list.Add(item.Key);
				}
			}
			for (int i = 0; i < list.Count; i++)
			{
				_recentChat.Remove(list[i]);
			}
		}
		string key = senderPeerId + "|" + text;
		if (_recentChat.TryGetValue(key, out var value) && realtimeSinceStartup - value < 3f)
		{
			return false;
		}
		_recentChat[key] = realtimeSinceStartup;
		return true;
	}
}
[HarmonyPatch]
internal static class RoutedRpcPatch
{
	private static MethodBase TargetMethod()
	{
		return AccessTools.Method(typeof(ZRoutedRpc), "RPC_RoutedRPC", new Type[2]
		{
			typeof(ZRpc),
			typeof(ZPackage)
		}, (Type[])null);
	}

	private static void Postfix(ZPackage pkg)
	{
		PatchGuard.Run("ZRoutedRpc.RPC_RoutedRPC", delegate
		{
			DeathboardPlugin instance = DeathboardPlugin.Instance;
			if ((Object)(object)instance != (Object)null)
			{
				instance.OnRoutedRpc(pkg);
			}
		});
	}
}
internal sealed class DeathRecord
{
	public string SteamId = "";

	public string Name = "";

	public int Deaths;

	public bool Counted;

	public string CountedZdo = "";

	public string FirstSeenUtc = "";

	public string LastSeenUtc = "";

	public float Combat;

	public float Acro;

	public float Hand;

	public float Power;

	public string SkillsUpdatedUtc = "";

	public bool HasSkills => !string.IsNullOrEmpty(SkillsUpdatedUtc);

	public SkillProfile Skills
	{
		get
		{
			if (!HasSkills)
			{
				return SkillProfile.Unknown;
			}
			return new SkillProfile
			{
				HasData = true,
				Combat = Combat,
				Acro = Acro,
				Hand = Hand
			};
		}
	}

	public string Key => DeathLedger.MakeKey(SteamId, Name);
}
internal sealed class DeathLedger
{
	private const int FileVersion = 1;

	private readonly Dictionary<string, DeathRecord> _records = new Dictionary<string, DeathRecord>(StringComparer.Ordinal);

	private readonly string _path;

	private readonly Action<string> _logInfo;

	private readonly Action<string> _logError;

	public string Path => _path;

	public int Count => _records.Count;

	public DeathLedger(string path, Action<string> logInfo, Action<string> logError)
	{
		_path = path;
		_logInfo = logInfo;
		_logError = logError;
	}

	public static string MakeKey(string steamId, string characterName)
	{
		return steamId + "|" + characterName;
	}

	private static string Num(float value)
	{
		return value.ToString("0.###", CultureInfo.InvariantCulture);
	}

	private static string UtcNow()
	{
		return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
	}

	public bool TryGet(string steamId, string name, out DeathRecord record)
	{
		return _records.TryGetValue(MakeKey(steamId, name), out record);
	}

	public bool EnsureCharacter(string steamId, string name, out DeathRecord record)
	{
		string key = MakeKey(steamId, name);
		if (_records.TryGetValue(key, out record))
		{
			record.LastSeenUtc = UtcNow();
			return false;
		}
		record = new DeathRecord
		{
			SteamId = (steamId ?? ""),
			Name = (name ?? ""),
			Deaths = 0,
			Counted = false,
			FirstSeenUtc = UtcNow(),
			LastSeenUtc = UtcNow()
		};
		_records[key] = record;
		return true;
	}

	public bool UpdateSkills(DeathRecord record, SkillProfile profile, PowerWeights weights)
	{
		if (!profile.HasData)
		{
			return false;
		}
		float num = PowerRating.Compute(profile, weights);
		bool result = !record.HasSkills || !Same(record.Combat, profile.Combat) || !Same(record.Acro, profile.Acro) || !Same(record.Hand, profile.Hand) || !Same(record.Power, num);
		record.Combat = profile.Combat;
		record.Acro = profile.Acro;
		record.Hand = profile.Hand;
		record.Power = num;
		record.SkillsUpdatedUtc = UtcNow();
		record.LastSeenUtc = record.SkillsUpdatedUtc;
		return result;
	}

	private static bool Same(float a, float b)
	{
		return Math.Abs(a - b) < 0.0005f;
	}

	public int IncrementDeaths(DeathRecord record, string corpseZdo)
	{
		record.Deaths++;
		record.Counted = true;
		record.CountedZdo = corpseZdo ?? "";
		record.LastSeenUtc = UtcNow();
		return record.Deaths;
	}

	public List<DeathRecord> Snapshot()
	{
		return new List<DeathRecord>(_records.Values);
	}

	public void Load()
	{
		try
		{
			string directoryName = System.IO.Path.GetDirectoryName(_path);
			if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			if (!File.Exists(_path))
			{
				_logInfo("No ledger at " + _path + " yet; starting a fresh one.");
				Save();
				return;
			}
			string text = File.ReadAllText(_path, Encoding.UTF8);
			if (string.IsNullOrEmpty(text.Trim()))
			{
				_logInfo("Ledger at " + _path + " was empty; starting a fresh one.");
				return;
			}
			object value;
			List<object> obj = MiniJson.AsArray((MiniJson.AsObject(MiniJson.Parse(text)) ?? throw new FormatException("Top-level value is not a JSON object.")).TryGetValue("entries", out value) ? value : null) ?? throw new FormatException("Missing 'entries' array.");
			_records.Clear();
			foreach (object item in obj)
			{
				Dictionary<string, object> dictionary = MiniJson.AsObject(item);
				if (dictionary != null)
				{
					DeathRecord deathRecord = new DeathRecord
					{
						SteamId = (MiniJson.GetString(dictionary, "steamId", "") ?? ""),
						Name = (MiniJson.GetString(dictionary, "name", "") ?? ""),
						Deaths = Math.Max(0, MiniJson.GetInt(dictionary, "deaths")),
						Counted = MiniJson.GetBool(dictionary, "counted"),
						CountedZdo = (MiniJson.GetString(dictionary, "countedZdo", "") ?? ""),
						FirstSeenUtc = (MiniJson.GetString(dictionary, "firstSeenUtc", "") ?? ""),
						LastSeenUtc = (MiniJson.GetString(dictionary, "lastSeenUtc", "") ?? ""),
						Combat = MiniJson.GetFloat(dictionary, "combat"),
						Acro = MiniJson.GetFloat(dictionary, "acro"),
						Hand = MiniJson.GetFloat(dictionary, "hand"),
						Power = MiniJson.GetFloat(dictionary, "power"),
						SkillsUpdatedUtc = (MiniJson.GetString(dictionary, "skillsUpdatedUtc", "") ?? "")
					};
					if (deathRecord.Name.Length != 0)
					{
						_records[deathRecord.Key] = deathRecord;
					}
				}
			}
			_logInfo("Loaded " + _records.Count + " character(s) from " + _path + ".");
		}
		catch (Exception ex)
		{
			_logError("Failed to load ledger '" + _path + "': " + ex.Message + " - starting empty and leaving the existing file untouched until the next write.");
		}
	}

	public void Save()
	{
		try
		{
			string directoryName = System.IO.Path.GetDirectoryName(_path);
			if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			StringBuilder stringBuilder = new StringBuilder(256 + _records.Count * 128);
			stringBuilder.Append("{\n  \"version\": ").Append(1).Append(",\n  \"entries\": [\n");
			List<DeathRecord> list = Snapshot();
			list.Sort(delegate(DeathRecord a, DeathRecord b)
			{
				int num2 = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
				return (num2 == 0) ? string.CompareOrdinal(a.SteamId, b.SteamId) : num2;
			});
			for (int num = 0; num < list.Count; num++)
			{
				DeathRecord deathRecord = list[num];
				stringBuilder.Append("    { \"steamId\": ");
				MiniJson.WriteString(stringBuilder, deathRecord.SteamId);
				stringBuilder.Append(", \"name\": ");
				MiniJson.WriteString(stringBuilder, deathRecord.Name);
				stringBuilder.Append(", \"deaths\": ").Append(deathRecord.Deaths.ToString(CultureInfo.InvariantCulture));
				stringBuilder.Append(", \"counted\": ").Append(deathRecord.Counted ? "true" : "false");
				stringBuilder.Append(", \"countedZdo\": ");
				MiniJson.WriteString(stringBuilder, deathRecord.CountedZdo);
				stringBuilder.Append(", \"firstSeenUtc\": ");
				MiniJson.WriteString(stringBuilder, deathRecord.FirstSeenUtc);
				stringBuilder.Append(", \"lastSeenUtc\": ");
				MiniJson.WriteString(stringBuilder, deathRecord.LastSeenUtc);
				stringBuilder.Append(", \"combat\": ").Append(Num(deathRecord.Combat));
				stringBuilder.Append(", \"acro\": ").Append(Num(deathRecord.Acro));
				stringBuilder.Append(", \"hand\": ").Append(Num(deathRecord.Hand));
				stringBuilder.Append(", \"power\": ").Append(Num(deathRecord.Power));
				stringBuilder.Append(", \"skillsUpdatedUtc\": ");
				MiniJson.WriteString(stringBuilder, deathRecord.SkillsUpdatedUtc);
				stringBuilder.Append(" }");
				if (num < list.Count - 1)
				{
					stringBuilder.Append(',');
				}
				stringBuilder.Append('\n');
			}
			stringBuilder.Append("  ]\n}\n");
			string text = _path + ".tmp";
			File.WriteAllText(text, stringBuilder.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
			if (File.Exists(_path))
			{
				File.Delete(_path);
			}
			File.Move(text, _path);
		}
		catch (Exception ex)
		{
			_logError("Failed to save ledger '" + _path + "': " + ex.Message);
		}
	}
}
internal sealed class PeerCharacter
{
	public long PeerId;

	public string SteamId = "";

	public string Name = "";

	public ZDOID CharacterId = ZDOID.None;

	public PlatformUserID PlatformId;

	public string LedgerKey => DeathLedger.MakeKey(SteamId, Name);
}
internal sealed class DeathWatch
{
	public const string OnDeathRpcName = "OnDeath";

	public static readonly int OnDeathRpcHash = StringExtensionMethods.GetStableHashCode("OnDeath");

	private readonly DeathLedger _ledger;

	private readonly Action<string> _logInfo;

	private readonly Action<string> _logWarning;

	private readonly SkillSource _skills;

	private readonly Func<PowerWeights> _weights;

	private readonly Dictionary<long, PeerCharacter> _online = new Dictionary<long, PeerCharacter>();

	public IEnumerable<PeerCharacter> Online => _online.Values;

	public event Action<PeerCharacter, DeathRecord, bool> CharacterJoined;

	public event Action<PeerCharacter, DeathRecord> CharacterDied;

	public event Action<PeerCharacter> CharacterLeft;

	public DeathWatch(DeathLedger ledger, SkillSource skills, Func<PowerWeights> weights, Action<string> logInfo, Action<string> logWarning)
	{
		_ledger = ledger;
		_skills = skills;
		_weights = weights;
		_logInfo = logInfo;
		_logWarning = logWarning;
	}

	public bool TryGetOnline(long peerId, out PeerCharacter character)
	{
		return _online.TryGetValue(peerId, out character);
	}

	public bool IsOnline(string steamId, string name)
	{
		string b = DeathLedger.MakeKey(steamId, name);
		foreach (PeerCharacter value in _online.Values)
		{
			if (string.Equals(value.LedgerKey, b, StringComparison.Ordinal))
			{
				return true;
			}
		}
		return false;
	}

	public static bool ServerActive()
	{
		if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
		{
			return ZRoutedRpc.instance != null;
		}
		return false;
	}

	private static bool TryDescribe(ZNetPeer peer, out PeerCharacter character)
	{
		//IL_0020: Unknown result type (might be due to invalid IL or missing references)
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		//IL_007a: Unknown result type (might be due to invalid IL or missing references)
		//IL_007f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0087: Unknown result type (might be due to invalid IL or missing references)
		//IL_008c: Unknown result type (might be due to invalid IL or missing references)
		character = null;
		if (peer == null || !peer.IsReady())
		{
			return false;
		}
		if (string.IsNullOrEmpty(peer.m_playerName))
		{
			return false;
		}
		if (peer.m_characterID == ZDOID.None)
		{
			return false;
		}
		if (peer.m_socket == null)
		{
			return false;
		}
		string hostName = peer.m_socket.GetHostName();
		if (string.IsNullOrEmpty(hostName))
		{
			return false;
		}
		character = new PeerCharacter
		{
			PeerId = peer.m_uid,
			SteamId = hostName,
			Name = peer.m_playerName,
			CharacterId = peer.m_characterID,
			PlatformId = ResolvePlatformId(peer, hostName)
		};
		return true;
	}

	private static PlatformUserID ResolvePlatformId(ZNetPeer peer, string hostName)
	{
		//IL_0070: 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_0078: Unknown result type (might be due to invalid IL or missing references)
		//IL_001f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0024: Unknown result type (might be due to invalid IL or missing references)
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0038: 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_0053: 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_005d: Unknown result type (might be due to invalid IL or missing references)
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance != (Object)null)
		{
			List<PlayerInfo> playerList = instance.GetPlayerList();
			if (playerList != null)
			{
				for (int i = 0; i < playerList.Count; i++)
				{
					if (playerList[i].m_characterID == peer.m_characterID)
					{
						PlayerInfo val = playerList[i];
						if (((PlatformUserID)(ref val.m_userInfo.m_id)).IsValid)
						{
							return playerList[i].m_userInfo.m_id;
						}
					}
				}
			}
		}
		if ((int)ZNet.m_onlineBackend != 0)
		{
			return new PlatformUserID(hostName);
		}
		return new PlatformUserID("Steam", hostName);
	}

	private static bool TryReadDeadFlag(ZDOID characterId, out bool dead)
	{
		//IL_000f: Unknown result type (might be due to invalid IL or missing references)
		dead = false;
		ZDOMan instance = ZDOMan.instance;
		if (instance == null)
		{
			return false;
		}
		ZDO zDO = instance.GetZDO(characterId);
		if (zDO == null || !zDO.IsValid())
		{
			return false;
		}
		dead = zDO.GetBool(ZDOVars.s_dead, false);
		return true;
	}

	public void Tick()
	{
		//IL_009e: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
		if (!ServerActive())
		{
			return;
		}
		List<ZNetPeer> peers = ZNet.instance.GetPeers();
		if (peers == null)
		{
			return;
		}
		HashSet<long> hashSet = new HashSet<long>();
		bool flag = false;
		for (int i = 0; i < peers.Count; i++)
		{
			if (TryDescribe(peers[i], out var character))
			{
				hashSet.Add(character.PeerId);
				if (!_online.TryGetValue(character.PeerId, out var value) || !string.Equals(value.LedgerKey, character.LedgerKey, StringComparison.Ordinal))
				{
					_online[character.PeerId] = character;
					flag |= HandleJoin(character);
					value = character;
				}
				else
				{
					value.CharacterId = character.CharacterId;
					value.PlatformId = character.PlatformId;
				}
				flag |= PollDeadFlag(value);
			}
		}
		List<long> list = null;
		foreach (long key in _online.Keys)
		{
			if (!hashSet.Contains(key))
			{
				if (list == null)
				{
					list = new List<long>();
				}
				list.Add(key);
			}
		}
		if (list != null)
		{
			for (int j = 0; j < list.Count; j++)
			{
				PeerCharacter obj = _online[list[j]];
				_logInfo(_online[list[j]].Name + " left; their ledger row is kept.");
				this.CharacterLeft?.Invoke(obj);
				_online.Remove(list[j]);
			}
		}
		if (flag)
		{
			_ledger.Save();
		}
	}

	public bool NoteOnDeathRpc(ZDOID targetZdo)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_002b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		//IL_003a: Unknown result type (might be due to invalid IL or missing references)
		if (targetZdo == ZDOID.None)
		{
			return false;
		}
		foreach (PeerCharacter value in _online.Values)
		{
			if (!(value.CharacterId != targetZdo))
			{
				if (!CountDeath(value, targetZdo, "OnDeath RPC"))
				{
					return false;
				}
				_ledger.Save();
				return true;
			}
		}
		return false;
	}

	public void RefreshSkills()
	{
		if (_skills == null || _online.Count == 0)
		{
			return;
		}
		bool flag = false;
		foreach (PeerCharacter value in _online.Values)
		{
			flag |= RefreshSkills(value);
		}
		if (flag)
		{
			_ledger.Save();
		}
	}

	private bool RefreshSkills(PeerCharacter character)
	{
		if (_skills == null)
		{
			return false;
		}
		try
		{
			if (!_skills.TryRead(character, out var profile))
			{
				return false;
			}
			if (!_ledger.TryGet(character.SteamId, character.Name, out var record))
			{
				return false;
			}
			return _ledger.UpdateSkills(record, profile, (_weights != null) ? _weights() : PowerWeights.Default);
		}
		catch (Exception ex)
		{
			_logWarning("Reading skills for " + character.Name + " failed: " + ex.Message);
			return false;
		}
	}

	private bool HandleJoin(PeerCharacter character)
	{
		DeathRecord record;
		bool flag = _ledger.EnsureCharacter(character.SteamId, character.Name, out record);
		RefreshSkills(character);
		_logInfo(character.Name + " joined (" + record.Deaths + " death(s)" + (flag ? ", new character" : "") + ").");
		this.CharacterJoined?.Invoke(character, record, flag);
		return true;
	}

	private bool PollDeadFlag(PeerCharacter character)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_004c: Unknown result type (might be due to invalid IL or missing references)
		if (!TryReadDeadFlag(character.CharacterId, out var dead))
		{
			return false;
		}
		if (!_ledger.TryGet(character.SteamId, character.Name, out var record))
		{
			_ledger.EnsureCharacter(character.SteamId, character.Name, out record);
		}
		if (dead)
		{
			return CountDeath(character, character.CharacterId, "ZDO dead flag");
		}
		if (record.Counted && string.IsNullOrEmpty(record.CountedZdo))
		{
			record.Counted = false;
			return true;
		}
		return false;
	}

	private unsafe bool CountDeath(PeerCharacter character, ZDOID corpse, string source)
	{
		if (!_ledger.TryGet(character.SteamId, character.Name, out var record))
		{
			_ledger.EnsureCharacter(character.SteamId, character.Name, out record);
		}
		string text = ((object)(*(ZDOID*)(&corpse))/*cast due to .constrained prefix*/).ToString();
		if (string.Equals(record.CountedZdo, text, StringComparison.Ordinal))
		{
			return false;
		}
		if (record.Counted && string.IsNullOrEmpty(record.CountedZdo))
		{
			return false;
		}
		int num = _ledger.IncrementDeaths(record, text);
		_logInfo(character.Name + " died (" + num + ") [via " + source + "]");
		this.CharacterDied?.Invoke(character, record);
		return true;
	}
}
internal sealed class DiscordWebhook
{
	private readonly Func<string> _urlProvider;

	private readonly Action<string> _logWarning;

	public DiscordWebhook(Func<string> urlProvider, Action<string> logWarning)
	{
		_urlProvider = urlProvider;
		_logWarning = logWarning;
	}

	public void Post(string content)
	{
		string url = ((_urlProvider != null) ? _urlProvider() : null);
		if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(content))
		{
			return;
		}
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("{\"content\":");
		MiniJson.WriteString(stringBuilder, content);
		stringBuilder.Append('}');
		byte[] body = Encoding.UTF8.GetBytes(stringBuilder.ToString());
		ThreadPool.QueueUserWorkItem(delegate
		{
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
				HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
				httpWebRequest.Method = "POST";
				httpWebRequest.ContentType = "application/json";
				httpWebRequest.UserAgent = "Deathboard/1.0 (Valheim dedicated server)";
				httpWebRequest.Timeout = 10000;
				httpWebRequest.ReadWriteTimeout = 10000;
				httpWebRequest.ContentLength = body.Length;
				using (Stream stream = httpWebRequest.GetRequestStream())
				{
					stream.Write(body, 0, body.Length);
				}
				using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
				int statusCode = (int)httpWebResponse.StatusCode;
				if (statusCode < 200 || statusCode >= 300)
				{
					_logWarning("Discord webhook returned HTTP " + statusCode + ".");
				}
			}
			catch (Exception ex)
			{
				_logWarning("Discord webhook failed: " + ex.Message);
			}
		});
	}
}
internal sealed class RealmServer
{
	internal struct StoredCharacter
	{
		public bool Found;

		public int Generation;

		public byte[] Blob;

		public byte[] World;
	}

	internal sealed class RealmPeer
	{
		public long PeerId;

		public int ClientBuild;

		public string SteamId = "";

		public string CharacterName = "";
	}

	private const float BoardRequestCooldownSeconds = 1f;

	private const float SkillReportCooldownSeconds = 5f;

	private const float CharUploadCooldownSeconds = 4f;

	private const float DeathReportCooldownSeconds = 2f;

	private const float ProfileRequestCooldownSeconds = 1f;

	private const float StatsReportCooldownSeconds = 15f;

	private readonly DeathboardConfig _config;

	private readonly Action<string> _logInfo;

	private readonly Action<string> _logWarning;

	private readonly Func<byte, byte, ZPackage> _buildBoard;

	private readonly Func<long, Dictionary<int, float>, bool> _applySkills;

	private readonly Func<long, RealmProtocol.CharUpload, bool> _applyCharUpload;

	private readonly Func<long, StoredCharacter> _loadCharacter;

	private readonly Func<long, RealmProtocol.DeathReport, bool> _applyDeathReport;

	private readonly Func<string, ZPackage> _buildProfile;

	private readonly Func<long, RealmProtocol.StatsReport, bool> _applyStats;

	private readonly Dictionary<long, float> _lastBoardRequest = new Dictionary<long, float>();

	private readonly Dictionary<long, float> _lastSkillReport = new Dictionary<long, float>();

	private readonly Dictionary<long, float> _lastCharUpload = new Dictionary<long, float>();

	private readonly Dictionary<long, float> _lastDeathReport = new Dictionary<long, float>();

	private readonly Dictionary<long, float> _lastProfileRequest = new Dictionary<long, float>();

	private readonly Dictionary<long, float> _lastStatsReport = new Dictionary<long, float>();

	private readonly Dictionary<long, RealmProtocol.UploadReason> _lastUploadReason = new Dictionary<long, RealmProtocol.UploadReason>();

	private readonly Dictionary<long, float> _firstSeen = new Dictionary<long, float>();

	private readonly Dictionary<long, float> _kicked = new Dictionary<long, float>();

	private const float KickLingerSeconds = 2f;

	private const float PruneIntervalSeconds = 5f;

	private float _lastPrune;

	private ZRoutedRpc _registeredOn;

	private readonly Dictionary<long, RealmPeer> _modded = new Dictionary<long, RealmPeer>();

	public static bool IsDedicatedServer => (int)SystemInfo.graphicsDeviceType == 4;

	public bool Enabled
	{
		get
		{
			if (_config != null && _config.RealmMode.Value)
			{
				return IsDedicatedServer;
			}
			return false;
		}
	}

	public bool RemoteBoardEnabled
	{
		get
		{
			if (_config != null)
			{
				return _config.RemoteBoardEnabled.Value;
			}
			return false;
		}
	}

	public bool AcceptSkillReports
	{
		get
		{
			if (_config != null)
			{
				return _config.AcceptSkillReports.Value;
			}
			return false;
		}
	}

	public bool AcceptDeathReports
	{
		get
		{
			if (_config != null)
			{
				return _config.AcceptDeathReports.Value;
			}
			return false;
		}
	}

	public bool AcceptStats
	{
		get
		{
			if (_config != null)
			{
				return _config.AcceptStats.Value;
			}
			return false;
		}
	}

	public bool AcceptCharUploads
	{
		get
		{
			if (Enabled)
			{
				return _config.AcceptCharUploads.Value;
			}
			return false;
		}
	}

	public bool KickUnmoddedEnabled
	{
		get
		{
			if (Enabled)
			{
				return _config.KickUnmodded.Value;
			}
			return false;
		}
	}

	public bool AnyRealmFeature
	{
		get
		{
			if (!Enabled && !RemoteBoardEnabled && !AcceptSkillReports && !AcceptDeathReports)
			{
				return AcceptStats;
			}
			return true;
		}
	}

	public int ModdedPeerCount => _modded.Count;

	public RealmServer(DeathboardConfig config, Func<byte, byte, ZPackage> buildBoard, Func<long, Dictionary<int, float>, bool> applySkills, Func<long, RealmProtocol.CharUpload, bool> applyCharUpload, Func<long, StoredCharacter> loadCharacter, Func<long, RealmProtocol.DeathReport, bool> applyDeathReport, Func<string, ZPackage> buildProfile, Func<long, RealmProtocol.StatsReport, bool> applyStats, Action<string> logInfo, Action<string> logWarning)
	{
		_config = config;
		_buildBoard = buildBoard;
		_applySkills = applySkills;
		_applyCharUpload = apply