Decompiled source of ValheimPlayerCards v0.2.1

ValheimPlayerCards.dll

Decompiled 19 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using UnityEngine;
using UnityEngine.Rendering;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("ValheimPlayerCards")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.1.0")]
[assembly: AssemblyInformationalVersion("0.2.1+1c5c4e8003d2c8c3266ddc9574b80a219acbfce3")]
[assembly: AssemblyProduct("ValheimPlayerCards")]
[assembly: AssemblyTitle("ValheimPlayerCards")]
[assembly: AssemblyVersion("0.2.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ValheimPlayerCards
{
	internal sealed class CardCommand : ConsoleCommand
	{
		public override string Name => "playercard";

		public override string Help => "Take a new portrait of your character and send your player card to the server now.";

		public override void Run(string[] args)
		{
			ClientSide.ForceNext();
			Console instance = Console.instance;
			if (instance != null)
			{
				instance.Print("ValheimPlayerCards: your card will be sent in a moment (the server accepts one every few seconds).");
			}
		}
	}
	internal static class ClientSide
	{
		private static readonly FieldRef<Humanoid, ItemData> RightItem = Field("m_rightItem");

		private static readonly FieldRef<Humanoid, ItemData> LeftItem = Field("m_leftItem");

		private static readonly FieldRef<Humanoid, ItemData> ChestItem = Field("m_chestItem");

		private static readonly FieldRef<Humanoid, ItemData> LegItem = Field("m_legItem");

		private static readonly FieldRef<Humanoid, ItemData> HelmetItem = Field("m_helmetItem");

		private static readonly FieldRef<Humanoid, ItemData> ShoulderItem = Field("m_shoulderItem");

		private static readonly FieldRef<Humanoid, ItemData> UtilityItem = Field("m_utilityItem");

		private static readonly FieldRef<Humanoid, ItemData> TrinketItem = Field("m_trinketItem");

		private static readonly FieldRef<Humanoid, ItemData> HiddenLeftItem = Field("m_hiddenLeftItem");

		private static readonly FieldRef<Humanoid, ItemData> HiddenRightItem = Field("m_hiddenRightItem");

		private static readonly FieldRef<Humanoid, string> BeardItem = AccessTools.FieldRefAccess<Humanoid, string>("m_beardItem");

		private static readonly FieldRef<Humanoid, string> HairItem = AccessTools.FieldRefAccess<Humanoid, string>("m_hairItem");

		private const float FirstDelay = 8f;

		private const float Debounce = 3f;

		private static ZNetPeer _serverPeer;

		private static bool _serverReady;

		private static int _serverMaxBytes;

		private static int _serverMinInterval;

		private static int _serverMaxSide;

		private static readonly HashSet<string> ServerIcons = new HashSet<string>();

		private static Player _player;

		private static float _playerSeenAt;

		private static string _sentSignature;

		private static string _pendingSignature;

		private static float _pendingSince;

		private static float _lastCapture = -10000f;

		private static float _nextCheck;

		private static bool _busy;

		private static bool _forced;

		private static int _uploadId = Environment.TickCount & 0x3FFFFFFF;

		private static FieldRef<Humanoid, ItemData> Field(string name)
		{
			return AccessTools.FieldRefAccess<Humanoid, ItemData>(name);
		}

		public static void RegisterPeer(ZNetPeer peer)
		{
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Expected O, but got Unknown
			if (!Plugin.IsHeadless)
			{
				_serverPeer = peer;
				_serverReady = false;
				peer.m_rpc.Register<ZPackage>("ValheimPlayerCards_Ready", (Action<ZRpc, ZPackage>)OnReady);
				ZPackage val = new ZPackage();
				val.Write(1);
				peer.m_rpc.Invoke("ValheimPlayerCards_Hello", new object[1] { val });
			}
		}

		public static void Reset()
		{
			_serverPeer = null;
			_serverReady = false;
			_player = null;
			_sentSignature = null;
			_pendingSignature = null;
		}

		private static void OnReady(ZRpc rpc, ZPackage pkg)
		{
			try
			{
				if (_serverPeer != null && rpc == _serverPeer.m_rpc && pkg.ReadInt() == 1)
				{
					_serverMaxBytes = pkg.ReadInt();
					_serverMinInterval = pkg.ReadInt();
					_serverMaxSide = pkg.ReadInt();
					ServerIcons.Clear();
					int num = Math.Min(pkg.ReadInt(), 2000);
					for (int i = 0; i < num; i++)
					{
						ServerIcons.Add(pkg.ReadString());
					}
					_serverReady = true;
					_sentSignature = null;
					Plugin.Log.LogInfo((object)$"Server accepts player cards (up to {_serverMaxBytes / 1024} KB, one every {_serverMinInterval} s).");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Bad answer from server: " + ex.Message));
			}
		}

		private static bool CanSend()
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return false;
			}
			if (ZNet.instance.IsServer())
			{
				return PlayerStore.Active;
			}
			if (_serverReady && _serverPeer != null)
			{
				return _serverPeer.m_rpc != null;
			}
			return false;
		}

		private static int Cooldown()
		{
			int num = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? Plugin.ServerMinIntervalSeconds.Value : _serverMinInterval);
			return Math.Max(Plugin.MinIntervalSeconds.Value, num + 2);
		}

		private static int ByteLimit()
		{
			int num = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? (Plugin.MaxUploadKB.Value * 1024) : _serverMaxBytes);
			return Math.Min(Plugin.TargetKB.Value * 1024, Math.Max(32768, num - 131072));
		}

		public static void ForceNext()
		{
			_forced = true;
			_sentSignature = null;
		}

		public static void Tick()
		{
			if (Plugin.IsHeadless || !Plugin.ClientEnabled.Value || _busy)
			{
				return;
			}
			float time = Time.time;
			if (time < _nextCheck)
			{
				return;
			}
			_nextCheck = time + 1f;
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || !CanSend())
			{
				return;
			}
			if ((Object)(object)localPlayer != (Object)(object)_player)
			{
				_player = localPlayer;
				_playerSeenAt = time;
				_sentSignature = null;
				_pendingSignature = null;
			}
			if (time - _playerSeenAt < 8f || ((Character)localPlayer).IsDead() || ((Character)localPlayer).IsTeleporting())
			{
				return;
			}
			string text;
			try
			{
				text = Signature(localPlayer);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Cannot read equipment: " + ex.Message));
				_nextCheck = time + 60f;
				return;
			}
			if (!(text == _sentSignature))
			{
				if (text != _pendingSignature)
				{
					_pendingSignature = text;
					_pendingSince = time;
				}
				if ((_forced || !(time - _pendingSince < 3f)) && (_forced || !(time - _lastCapture < (float)Cooldown())))
				{
					_forced = false;
					_busy = true;
					_lastCapture = time;
					((MonoBehaviour)Plugin.Instance).StartCoroutine(CaptureAndSend(localPlayer, text));
				}
			}
		}

		private static string Signature(Player p)
		{
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01df: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			StringBuilder stringBuilder = new StringBuilder();
			foreach (var (text, item) in Equipped(p))
			{
				switch (text)
				{
				case "right":
				case "left":
				case "backRight":
				case "backLeft":
					continue;
				}
				stringBuilder.Append(text).Append('=').Append(ItemKey(item))
					.Append(';');
			}
			List<string> list = new List<string>
			{
				ItemKey(RightItem.Invoke((Humanoid)(object)p)),
				ItemKey(LeftItem.Invoke((Humanoid)(object)p)),
				ItemKey(HiddenRightItem.Invoke((Humanoid)(object)p)),
				ItemKey(HiddenLeftItem.Invoke((Humanoid)(object)p))
			};
			list.Sort(StringComparer.Ordinal);
			stringBuilder.Append("weapons=").Append(string.Join(",", list)).Append(';');
			stringBuilder.Append("hair=").Append(HairItem.Invoke((Humanoid)(object)p)).Append(";beard=")
				.Append(BeardItem.Invoke((Humanoid)(object)p))
				.Append(';');
			ZNetView component = ((Component)p).GetComponent<ZNetView>();
			ZDO val = ((component != null) ? component.GetZDO() : null);
			if (val != null)
			{
				stringBuilder.Append("model=").Append(val.GetInt(ZDOVars.s_modelIndex, 0)).Append(';');
				stringBuilder.Append("skin=").Append(val.GetVec3(ZDOVars.s_skinColor, Vector3.one)).Append(';');
				stringBuilder.Append("haircolor=").Append(val.GetVec3(ZDOVars.s_hairColor, Vector3.one)).Append(';');
			}
			return stringBuilder.ToString();
		}

		private static string ItemKey(ItemData item)
		{
			if (item == null)
			{
				return "";
			}
			string arg = (((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : item.m_shared?.m_name);
			return $"{arg}:{item.m_quality}:{item.m_variant}";
		}

		private static IEnumerable<(string, ItemData)> Equipped(Player p)
		{
			yield return ("helmet", HelmetItem.Invoke((Humanoid)(object)p));
			yield return ("chest", ChestItem.Invoke((Humanoid)(object)p));
			yield return ("legs", LegItem.Invoke((Humanoid)(object)p));
			yield return ("cape", ShoulderItem.Invoke((Humanoid)(object)p));
			yield return ("utility", UtilityItem.Invoke((Humanoid)(object)p));
			yield return ("trinket", TrinketItem.Invoke((Humanoid)(object)p));
			yield return ("right", RightItem.Invoke((Humanoid)(object)p));
			yield return ("left", LeftItem.Invoke((Humanoid)(object)p));
			yield return ("backRight", HiddenRightItem.Invoke((Humanoid)(object)p));
			yield return ("backLeft", HiddenLeftItem.Invoke((Humanoid)(object)p));
		}

		private static IEnumerator CaptureAndSend(Player player, string sig)
		{
			try
			{
				byte[] png = null;
				yield return PortraitRenderer.Render(player, ByteLimit(), (_serverMaxSide > 0) ? _serverMaxSide : 2048, delegate(byte[] result)
				{
					png = result;
				});
				_sentSignature = sig;
				if (png == null || (Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer || !CanSend())
				{
					yield break;
				}
				CardPayload cardPayload = BuildPayload(player, png);
				AddIcons(player, cardPayload);
				byte[] array = Protocol.Serialize(cardPayload);
				if (Plugin.SaveLocalCopy.Value)
				{
					SaveLocal(png);
				}
				Send(player, array);
				foreach (IconInfo icon in cardPayload.Icons)
				{
					ServerIcons.Add(icon.Key);
				}
				Plugin.Log.LogInfo((object)$"Card sent ({array.Length / 1024} KB: portrait {png.Length / 1024} KB, {cardPayload.Items.Count} items, {cardPayload.Skills.Count} skills, {cardPayload.Stats.Count} stats, {cardPayload.Icons.Count} icons).");
			}
			finally
			{
				_busy = false;
			}
		}

		private static CardPayload BuildPayload(Player p, byte[] png)
		{
			//IL_0155: Unknown result type (might be due to invalid IL or missing references)
			//IL_015a: Unknown result type (might be due to invalid IL or missing references)
			//IL_015f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0167: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0171: Unknown result type (might be due to invalid IL or missing references)
			//IL_017c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_0190: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_032f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0339: Invalid comparison between Unknown and I4
			//IL_033d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0347: Invalid comparison between Unknown and I4
			//IL_0357: Unknown result type (might be due to invalid IL or missing references)
			//IL_035c: Unknown result type (might be due to invalid IL or missing references)
			CardPayload cardPayload = new CardPayload
			{
				Png = png,
				Hair = (HairItem.Invoke((Humanoid)(object)p) ?? ""),
				Beard = (BeardItem.Invoke((Humanoid)(object)p) ?? "")
			};
			foreach (var (slot, val) in Equipped(p))
			{
				if (val != null)
				{
					string text = val.m_shared?.m_name ?? "";
					cardPayload.Items.Add(new ItemInfo
					{
						Slot = slot,
						Prefab = (((Object)(object)val.m_dropPrefab != (Object)null) ? ((Object)val.m_dropPrefab).name : ""),
						Name = ((Localization.instance != null) ? Localization.instance.Localize(text) : text),
						Token = text,
						Quality = val.m_quality,
						Variant = val.m_variant
					});
				}
			}
			ZNetView component = ((Component)p).GetComponent<ZNetView>();
			ZDO val2 = ((component != null) ? component.GetZDO() : null);
			if (val2 != null)
			{
				cardPayload.Model = val2.GetInt(ZDOVars.s_modelIndex, 0);
				Vector3 vec = val2.GetVec3(ZDOVars.s_skinColor, Vector3.one);
				Vector3 vec2 = val2.GetVec3(ZDOVars.s_hairColor, Vector3.one);
				cardPayload.SkinColor = new float[3] { vec.x, vec.y, vec.z };
				cardPayload.HairColor = new float[3] { vec2.x, vec2.y, vec2.z };
			}
			if (Plugin.ShareSkills.Value)
			{
				foreach (Skill skill in ((Character)p).GetSkills().GetSkillList())
				{
					if (skill?.m_info != null)
					{
						string text2 = ((object)Unsafe.As<SkillType, SkillType>(ref skill.m_info.m_skill)/*cast due to .constrained prefix*/).ToString();
						string text3 = "$skill_" + text2.ToLowerInvariant();
						string text4 = ((Localization.instance != null) ? Localization.instance.Localize(text3) : text2);
						cardPayload.Skills.Add(new SkillInfo
						{
							Id = text2,
							Name = (text4.StartsWith("[") ? text2 : text4),
							Level = skill.m_level
						});
					}
				}
			}
			PlayerProfile val3 = (((Object)(object)Game.instance != (Object)null) ? Game.instance.GetPlayerProfile() : null);
			if (Plugin.ShareStats.Value && val3?.m_playerStats != null && val3.m_playerStats.Length != 0 && val3.m_playerStats[0] != null)
			{
				foreach (KeyValuePair<PlayerStatType, float> stat in val3.m_playerStats[0].m_stats)
				{
					if (stat.Value != 0f && (int)stat.Key != 205 && (int)stat.Key != 206)
					{
						cardPayload.Stats.Add(new StatInfo
						{
							Id = ((object)stat.Key/*cast due to .constrained prefix*/).ToString(),
							Value = stat.Value
						});
						if (cardPayload.Stats.Count >= 512)
						{
							break;
						}
					}
				}
			}
			return cardPayload;
		}

		private static void AddIcons(Player p, CardPayload payload)
		{
			foreach (var item2 in Equipped(p))
			{
				ItemData item = item2.Item2;
				if (item == null || (Object)(object)item.m_dropPrefab == (Object)null || payload.Icons.Count >= 24)
				{
					continue;
				}
				string key = PlayerStore.IconKey(((Object)item.m_dropPrefab).name, item.m_variant);
				if (ServerIcons.Contains(key) || payload.Icons.Any((IconInfo i) => i.Key == key))
				{
					continue;
				}
				try
				{
					byte[] array = IconRenderer.Render(item.GetIcon());
					if (array != null && array.Length <= 49152)
					{
						payload.Icons.Add(new IconInfo
						{
							Key = key,
							Png = array
						});
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("Cannot draw the icon of " + ((Object)item.m_dropPrefab).name + ": " + ex.Message));
				}
			}
		}

		private static void Send(Player player, byte[] bytes)
		{
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_006c: Expected O, but got Unknown
			if (ZNet.instance.IsServer())
			{
				ServerSide.Save(player.GetPlayerName(), bytes);
				return;
			}
			int num = _uploadId++;
			int num2 = Protocol.ChunkCount(bytes.Length);
			for (int i = 0; i < num2; i++)
			{
				int num3 = i * 60000;
				int num4 = Math.Min(60000, bytes.Length - num3);
				byte[] array = new byte[num4];
				Buffer.BlockCopy(bytes, num3, array, 0, num4);
				ZPackage val = new ZPackage();
				val.Write(1);
				val.Write(num);
				val.Write(i);
				val.Write(num2);
				val.Write(bytes.Length);
				val.Write(array);
				_serverPeer.m_rpc.Invoke("ValheimPlayerCards_Upload", new object[1] { val });
			}
		}

		private static void SaveLocal(byte[] png)
		{
			try
			{
				string text = Path.Combine(Paths.BepInExRootPath, "ValheimPlayerCards");
				Directory.CreateDirectory(text);
				File.WriteAllBytes(Path.Combine(text, "last-portrait.png"), png);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Cannot save local copy: " + ex.Message));
			}
		}
	}
	internal static class IconRenderer
	{
		private const int Size = 64;

		public static byte[] Render(Sprite sprite)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_0123: Unknown result type (might be due to invalid IL or missing references)
			//IL_0138: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Expected O, but got Unknown
			//IL_0155: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)sprite == (Object)null || (Object)(object)sprite.texture == (Object)null)
			{
				return null;
			}
			Texture texture = (Texture)(object)sprite.texture;
			Rect textureRect = sprite.textureRect;
			Rect val = default(Rect);
			((Rect)(ref val))..ctor(((Rect)(ref textureRect)).x / (float)texture.width, ((Rect)(ref textureRect)).y / (float)texture.height, ((Rect)(ref textureRect)).width / (float)texture.width, ((Rect)(ref textureRect)).height / (float)texture.height);
			float num = ((Rect)(ref textureRect)).width / ((Rect)(ref textureRect)).height;
			Rect val2 = ((num >= 1f) ? new Rect(0f, (64f - 64f / num) / 2f, 64f, 64f / num) : new Rect((64f - 64f * num) / 2f, 0f, 64f * num, 64f));
			RenderTexture temporary = RenderTexture.GetTemporary(64, 64, 0, (RenderTextureFormat)0);
			RenderTexture active = RenderTexture.active;
			Texture2D val3 = null;
			try
			{
				RenderTexture.active = temporary;
				GL.Clear(true, true, Color.clear);
				GL.PushMatrix();
				GL.LoadPixelMatrix(0f, 64f, 64f, 0f);
				Graphics.DrawTexture(val2, texture, val, 0, 0, 0, 0);
				GL.PopMatrix();
				val3 = new Texture2D(64, 64, (TextureFormat)4, false);
				val3.ReadPixels(new Rect(0f, 0f, 64f, 64f), 0, 0);
				val3.Apply(false);
				return ImageConversion.EncodeToPNG(val3);
			}
			finally
			{
				RenderTexture.active = active;
				RenderTexture.ReleaseTemporary(temporary);
				if ((Object)(object)val3 != (Object)null)
				{
					Object.Destroy((Object)(object)val3);
				}
			}
		}
	}
	internal static class Json
	{
		public static string Write(object value, bool pretty = false)
		{
			StringBuilder stringBuilder = new StringBuilder();
			WriteValue(stringBuilder, value, pretty, 0);
			return stringBuilder.ToString();
		}

		private static void WriteValue(StringBuilder sb, object v, bool pretty, int depth)
		{
			if (v != null)
			{
				if (!(v is string s))
				{
					if (!(v is bool flag))
					{
						if (!(v is int num))
						{
							if (!(v is long num2))
							{
								if (!(v is float num3))
								{
									if (!(v is double d))
									{
										if (!(v is IDictionary<string, object> dictionary))
										{
											if (v is IEnumerable enumerable)
											{
												sb.Append('[');
												bool flag2 = true;
												foreach (object item in enumerable)
												{
													if (!flag2)
													{
														sb.Append(',');
													}
													flag2 = false;
													NewLine(sb, pretty, depth + 1);
													WriteValue(sb, item, pretty, depth + 1);
												}
												if (!flag2)
												{
													NewLine(sb, pretty, depth);
												}
												sb.Append(']');
											}
											else
											{
												WriteString(sb, Convert.ToString(v, CultureInfo.InvariantCulture));
											}
											return;
										}
										sb.Append('{');
										bool flag3 = true;
										foreach (KeyValuePair<string, object> item2 in dictionary)
										{
											if (!flag3)
											{
												sb.Append(',');
											}
											flag3 = false;
											NewLine(sb, pretty, depth + 1);
											WriteString(sb, item2.Key);
											sb.Append(pretty ? ": " : ":");
											WriteValue(sb, item2.Value, pretty, depth + 1);
										}
										if (!flag3)
										{
											NewLine(sb, pretty, depth);
										}
										sb.Append('}');
									}
									else
									{
										WriteNumber(sb, d);
									}
								}
								else
								{
									WriteNumber(sb, num3);
								}
							}
							else
							{
								sb.Append(num2.ToString(CultureInfo.InvariantCulture));
							}
						}
						else
						{
							sb.Append(num.ToString(CultureInfo.InvariantCulture));
						}
					}
					else
					{
						sb.Append(flag ? "true" : "false");
					}
				}
				else
				{
					WriteString(sb, s);
				}
			}
			else
			{
				sb.Append("null");
			}
		}

		private static void NewLine(StringBuilder sb, bool pretty, int depth)
		{
			if (pretty)
			{
				sb.Append('\n').Append(' ', depth * 2);
			}
		}

		private static void WriteNumber(StringBuilder sb, double d)
		{
			if (double.IsNaN(d) || double.IsInfinity(d))
			{
				d = 0.0;
			}
			if (Math.Abs(d % 1.0) < 1E-09 && Math.Abs(d) < 1000000000000000.0)
			{
				sb.Append(((long)Math.Round(d)).ToString(CultureInfo.InvariantCulture));
			}
			else
			{
				sb.Append(d.ToString("0.###", CultureInfo.InvariantCulture));
			}
		}

		private static void WriteString(StringBuilder sb, string s)
		{
			sb.Append('"');
			string text = s ?? "";
			foreach (char c in text)
			{
				switch (c)
				{
				case '"':
					sb.Append("\\\"");
					continue;
				case '\\':
					sb.Append("\\\\");
					continue;
				case '\n':
					sb.Append("\\n");
					continue;
				case '\r':
					sb.Append("\\r");
					continue;
				case '\t':
					sb.Append("\\t");
					continue;
				}
				if (c < ' ' || c == '<' || c == '>' || c == '&' || c == '\u2028' || c == '\u2029')
				{
					StringBuilder stringBuilder = sb.Append("\\u");
					int num = c;
					stringBuilder.Append(num.ToString("x4"));
				}
				else
				{
					sb.Append(c);
				}
			}
			sb.Append('"');
		}

		public static object Parse(string text)
		{
			int pos = 0;
			object result = ParseValue(text, ref pos, 0);
			SkipSpace(text, ref pos);
			if (pos != text.Length)
			{
				throw new FormatException("trailing characters");
			}
			return result;
		}

		private static object ParseValue(string s, ref int pos, int depth)
		{
			if (depth > 32)
			{
				throw new FormatException("too deep");
			}
			SkipSpace(s, ref pos);
			if (pos >= s.Length)
			{
				throw new FormatException("unexpected end");
			}
			switch (s[pos])
			{
			case '{':
			{
				pos++;
				Dictionary<string, object> dictionary = new Dictionary<string, object>();
				SkipSpace(s, ref pos);
				if (Peek(s, pos) == '}')
				{
					pos++;
					return dictionary;
				}
				while (true)
				{
					SkipSpace(s, ref pos);
					string key = ParseString(s, ref pos);
					SkipSpace(s, ref pos);
					Expect(s, ref pos, ':');
					dictionary[key] = ParseValue(s, ref pos, depth + 1);
					SkipSpace(s, ref pos);
					if (Peek(s, pos) != ',')
					{
						break;
					}
					pos++;
				}
				Expect(s, ref pos, '}');
				return dictionary;
			}
			case '[':
			{
				pos++;
				List<object> list = new List<object>();
				SkipSpace(s, ref pos);
				if (Peek(s, pos) == ']')
				{
					pos++;
					return list;
				}
				while (true)
				{
					list.Add(ParseValue(s, ref pos, depth + 1));
					SkipSpace(s, ref pos);
					if (Peek(s, pos) != ',')
					{
						break;
					}
					pos++;
				}
				Expect(s, ref pos, ']');
				return list;
			}
			case '"':
				return ParseString(s, ref pos);
			default:
			{
				if (Match(s, ref pos, "true"))
				{
					return true;
				}
				if (Match(s, ref pos, "false"))
				{
					return false;
				}
				if (Match(s, ref pos, "null"))
				{
					return null;
				}
				int num = pos;
				while (pos < s.Length && "+-0123456789.eE".IndexOf(s[pos]) >= 0)
				{
					pos++;
				}
				if (pos == num)
				{
					throw new FormatException("unexpected character");
				}
				return double.Parse(s.Substring(num, pos - num), NumberStyles.Float, CultureInfo.InvariantCulture);
			}
			}
		}

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

		private static void SkipSpace(string s, ref int pos)
		{
			while (pos < s.Length && char.IsWhiteSpace(s[pos]))
			{
				pos++;
			}
		}

		private static char Peek(string s, int pos)
		{
			if (pos >= s.Length)
			{
				return '\0';
			}
			return s[pos];
		}

		private static void Expect(string s, ref int pos, char c)
		{
			if (Peek(s, pos) != c)
			{
				throw new FormatException($"expected '{c}'");
			}
			pos++;
		}

		private static bool Match(string s, ref int pos, string word)
		{
			if (string.CompareOrdinal(s, pos, word, 0, word.Length) != 0)
			{
				return false;
			}
			pos += word.Length;
			return true;
		}

		public static string Str(IDictionary<string, object> o, string key)
		{
			if (o == null || !o.TryGetValue(key, out var value) || !(value is string result))
			{
				return "";
			}
			return result;
		}

		public static double Num(IDictionary<string, object> o, string key)
		{
			if (o != null && o.TryGetValue(key, out var value) && value is double)
			{
				return (double)value;
			}
			return 0.0;
		}

		public static long Long(IDictionary<string, object> o, string key)
		{
			return (long)Num(o, key);
		}

		public static int Int(IDictionary<string, object> o, string key)
		{
			return (int)Num(o, key);
		}

		public static IEnumerable<IDictionary<string, object>> Objects(IDictionary<string, object> o, string key)
		{
			if (o == null || !o.TryGetValue(key, out var value) || !(value is List<object> list))
			{
				yield break;
			}
			foreach (object item in list)
			{
				if (item is IDictionary<string, object> dictionary)
				{
					yield return dictionary;
				}
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Awake")]
	internal static class ZNet_Awake
	{
		private static void Postfix(ZNet __instance)
		{
			try
			{
				if (__instance.IsServer())
				{
					ServerSide.Start();
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Server start: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class ZNet_OnNewConnection
	{
		private static void Postfix(ZNet __instance, ZNetPeer __0)
		{
			try
			{
				if (__instance.IsServer())
				{
					ServerSide.RegisterPeer(__0);
				}
				else
				{
					ClientSide.RegisterPeer(__0);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"OnNewConnection: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_CharacterID")]
	internal static class ZNet_RPC_CharacterID
	{
		private static void Postfix(ZNet __instance, ZRpc __0)
		{
			try
			{
				if (!__instance.IsServer())
				{
					return;
				}
				foreach (ZNetPeer peer in __instance.GetPeers())
				{
					if (peer.m_rpc == __0)
					{
						PlayerStore.OnPlayerJoined(peer);
						break;
					}
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"RPC_CharacterID: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	internal static class ZNet_Disconnect
	{
		private static void Prefix(ZNet __instance, ZNetPeer peer)
		{
			try
			{
				if (__instance.IsServer())
				{
					ServerSide.OnPeerLeft(peer);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Disconnect: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnDestroy")]
	internal static class ZNet_OnDestroy
	{
		private static void Prefix()
		{
			ServerSide.Stop();
			ClientSide.Reset();
		}
	}
	internal class ItemRecord
	{
		public string slot = "";

		public string prefab = "";

		public string name = "";

		public string token = "";

		public string icon = "";

		public int quality;

		public int variant;
	}
	internal class SkillRecord
	{
		public string id = "";

		public string name = "";

		public float level;
	}
	internal class StatRecord
	{
		public string id = "";

		public float value;
	}
	internal class SessionRecord
	{
		public long start;

		public long end;
	}
	internal class PlayerRecord
	{
		public string id = "";

		public string name = "";

		public long firstSeen;

		public long lastSeen;

		public double playSeconds;

		public int sessions;

		public long openSince;

		public List<SessionRecord> recent = new List<SessionRecord>();

		public long cardUpdated;

		public int portraitWidth;

		public int portraitHeight;

		public int model;

		public string hair = "";

		public string beard = "";

		public string skinColor = "";

		public string hairColor = "";

		public List<ItemRecord> items = new List<ItemRecord>();

		public List<SkillRecord> skills = new List<SkillRecord>();

		public List<StatRecord> stats = new List<StatRecord>();

		public Dictionary<string, object> ToJson()
		{
			return new Dictionary<string, object>
			{
				["id"] = id,
				["name"] = name,
				["firstSeen"] = firstSeen,
				["lastSeen"] = lastSeen,
				["playSeconds"] = playSeconds,
				["sessions"] = sessions,
				["openSince"] = openSince,
				["recent"] = recent.Select((SessionRecord r) => new Dictionary<string, object>
				{
					["start"] = r.start,
					["end"] = r.end
				}).ToList(),
				["cardUpdated"] = cardUpdated,
				["portraitWidth"] = portraitWidth,
				["portraitHeight"] = portraitHeight,
				["model"] = model,
				["hair"] = hair,
				["beard"] = beard,
				["skinColor"] = skinColor,
				["hairColor"] = hairColor,
				["items"] = items.Select((ItemRecord i) => new Dictionary<string, object>
				{
					["slot"] = i.slot,
					["prefab"] = i.prefab,
					["name"] = i.name,
					["token"] = i.token,
					["icon"] = i.icon,
					["quality"] = i.quality,
					["variant"] = i.variant
				}).ToList(),
				["skills"] = skills.Select((SkillRecord k) => new Dictionary<string, object>
				{
					["id"] = k.id,
					["name"] = k.name,
					["level"] = k.level
				}).ToList(),
				["stats"] = stats.Select((StatRecord k) => new Dictionary<string, object>
				{
					["id"] = k.id,
					["value"] = k.value
				}).ToList()
			};
		}

		public static PlayerRecord FromJson(IDictionary<string, object> o)
		{
			return new PlayerRecord
			{
				id = Json.Str(o, "id"),
				name = Json.Str(o, "name"),
				firstSeen = Json.Long(o, "firstSeen"),
				lastSeen = Json.Long(o, "lastSeen"),
				playSeconds = Json.Num(o, "playSeconds"),
				sessions = Json.Int(o, "sessions"),
				openSince = Json.Long(o, "openSince"),
				recent = (from r in Json.Objects(o, "recent")
					select new SessionRecord
					{
						start = Json.Long(r, "start"),
						end = Json.Long(r, "end")
					}).ToList(),
				cardUpdated = Json.Long(o, "cardUpdated"),
				portraitWidth = Json.Int(o, "portraitWidth"),
				portraitHeight = Json.Int(o, "portraitHeight"),
				model = Json.Int(o, "model"),
				hair = Json.Str(o, "hair"),
				beard = Json.Str(o, "beard"),
				skinColor = Json.Str(o, "skinColor"),
				hairColor = Json.Str(o, "hairColor"),
				items = (from i in Json.Objects(o, "items")
					select new ItemRecord
					{
						slot = Json.Str(i, "slot"),
						prefab = Json.Str(i, "prefab"),
						name = Json.Str(i, "name"),
						token = Json.Str(i, "token"),
						icon = Json.Str(i, "icon"),
						quality = Json.Int(i, "quality"),
						variant = Json.Int(i, "variant")
					}).ToList(),
				skills = (from k in Json.Objects(o, "skills")
					select new SkillRecord
					{
						id = Json.Str(k, "id"),
						name = Json.Str(k, "name"),
						level = (float)Json.Num(k, "level")
					}).ToList(),
				stats = (from k in Json.Objects(o, "stats")
					select new StatRecord
					{
						id = Json.Str(k, "id"),
						value = (float)Json.Num(k, "value")
					}).ToList()
			};
		}
	}
	internal sealed class Snapshot
	{
		public string Directory = "";

		public Dictionary<string, byte[]> Json = new Dictionary<string, byte[]>();

		public HashSet<string> Portraits = new HashSet<string>();

		public HashSet<string> Icons = new HashSet<string>();
	}
	internal static class PlayerStore
	{
		private sealed class Online
		{
			public string Id;

			public long Start;
		}

		private sealed class Features
		{
			public bool PlayTime;

			public bool Equipment;

			public bool ItemIcons;

			public bool Skills;

			public bool Stats;
		}

		private const int RecentSessions = 30;

		private const float RefreshSeconds = 30f;

		private const float HeartbeatSeconds = 60f;

		private static readonly Dictionary<string, PlayerRecord> Players = new Dictionary<string, PlayerRecord>();

		private static readonly Dictionary<long, Online> OnlineByPeer = new Dictionary<long, Online>();

		private static readonly HashSet<string> IconKeys = new HashSet<string>();

		private static string _dir;

		private static bool _dirty;

		private static float _nextRefresh;

		private static float _nextHeartbeat;

		public static volatile Snapshot Current;

		public static bool Active => _dir != null;

		public static IEnumerable<string> KnownIcons => IconKeys;

		private static long Now => DateTimeOffset.UtcNow.ToUnixTimeSeconds();

		public static void Start()
		{
			_dir = ResolveDirectory();
			Directory.CreateDirectory(Path.Combine(_dir, "players"));
			Directory.CreateDirectory(Path.Combine(_dir, "portraits"));
			Directory.CreateDirectory(Path.Combine(_dir, "icons"));
			Players.Clear();
			OnlineByPeer.Clear();
			IconKeys.Clear();
			string[] files = Directory.GetFiles(Path.Combine(_dir, "players"), "*.json");
			foreach (string path in files)
			{
				try
				{
					PlayerRecord playerRecord = PlayerRecord.FromJson(Json.Parse(File.ReadAllText(path, Encoding.UTF8)) as IDictionary<string, object>);
					if (playerRecord != null && !string.IsNullOrEmpty(playerRecord.id) && !(playerRecord.id != Path.GetFileNameWithoutExtension(path)))
					{
						if (playerRecord.openSince > 0)
						{
							CloseSession(playerRecord, Math.Max(playerRecord.openSince, playerRecord.lastSeen));
							Save(playerRecord);
						}
						Players[playerRecord.id] = playerRecord;
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("Cannot read " + Path.GetFileName(path) + ": " + ex.Message));
				}
			}
			files = Directory.GetFiles(Path.Combine(_dir, "icons"), "*.png");
			foreach (string path2 in files)
			{
				IconKeys.Add(Path.GetFileNameWithoutExtension(path2));
			}
			_dirty = true;
			Rebuild();
			Plugin.Log.LogInfo((object)$"Player cards in {_dir}: {Players.Count} players, {IconKeys.Count} item icons.");
		}

		public static void Stop()
		{
			if (_dir == null)
			{
				return;
			}
			long now = Now;
			foreach (Online value2 in OnlineByPeer.Values)
			{
				if (Players.TryGetValue(value2.Id, out var value))
				{
					CloseSession(value, now);
					Save(value);
				}
			}
			OnlineByPeer.Clear();
			_dir = null;
			Current = null;
		}

		public static void Tick()
		{
			if (_dir == null)
			{
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (OnlineByPeer.Count > 0 && realtimeSinceStartup >= _nextHeartbeat)
			{
				_nextHeartbeat = realtimeSinceStartup + 60f;
				long now = Now;
				foreach (Online value2 in OnlineByPeer.Values)
				{
					if (Players.TryGetValue(value2.Id, out var value))
					{
						value.lastSeen = now;
						Save(value);
					}
				}
			}
			if (_dirty || realtimeSinceStartup >= _nextRefresh)
			{
				Rebuild();
			}
		}

		private static string ResolveDirectory()
		{
			string value = Plugin.DataDirectory.Value;
			if (string.IsNullOrWhiteSpace(value))
			{
				string name = ((ZNet.World != null) ? ZNet.World.m_name : "world");
				return Path.Combine(Paths.ConfigPath, "ValheimPlayerCards", SafeName(name));
			}
			if (!Path.IsPathRooted(value))
			{
				return Path.Combine(Paths.GameRootPath, value);
			}
			return value;
		}

		public static void OnPlayerJoined(ZNetPeer peer)
		{
			if (_dir != null && peer != null && !string.IsNullOrEmpty(peer.m_playerName) && !OnlineByPeer.ContainsKey(peer.m_uid))
			{
				long now = Now;
				PlayerRecord orCreate = GetOrCreate(peer.m_playerName);
				if (orCreate.firstSeen == 0L)
				{
					orCreate.firstSeen = now;
				}
				if (orCreate.openSince > 0)
				{
					CloseSession(orCreate, Math.Max(orCreate.openSince, orCreate.lastSeen));
				}
				orCreate.openSince = now;
				orCreate.lastSeen = now;
				orCreate.sessions++;
				OnlineByPeer[peer.m_uid] = new Online
				{
					Id = orCreate.id,
					Start = now
				};
				Save(orCreate);
				_dirty = true;
			}
		}

		public static void OnPeerLeft(ZNetPeer peer)
		{
			if (_dir != null && peer != null && OnlineByPeer.TryGetValue(peer.m_uid, out var value))
			{
				OnlineByPeer.Remove(peer.m_uid);
				if (Players.TryGetValue(value.Id, out var value2))
				{
					CloseSession(value2, Now);
					Save(value2);
				}
				_dirty = true;
			}
		}

		private static void CloseSession(PlayerRecord rec, long end)
		{
			if (rec.openSince <= 0)
			{
				return;
			}
			long openSince = rec.openSince;
			if (end > openSince)
			{
				rec.playSeconds += end - openSince;
				rec.recent.Add(new SessionRecord
				{
					start = openSince,
					end = end
				});
				if (rec.recent.Count > 30)
				{
					rec.recent.RemoveRange(0, rec.recent.Count - 30);
				}
			}
			rec.lastSeen = Math.Max(rec.lastSeen, end);
			rec.openSince = 0L;
		}

		public static void SaveCard(string playerName, CardPayload p, int width, int height)
		{
			if (_dir == null)
			{
				return;
			}
			PlayerRecord orCreate = GetOrCreate(playerName);
			long now = Now;
			if (orCreate.firstSeen == 0L)
			{
				orCreate.firstSeen = now;
			}
			orCreate.lastSeen = Math.Max(orCreate.lastSeen, now);
			WriteAtomic(Path.Combine(_dir, "portraits", orCreate.id + ".png"), p.Png);
			int num = 0;
			HashSet<string> hashSet = new HashSet<string>(p.Items.Select((ItemInfo i) => IconKey(i.Prefab, i.Variant)));
			foreach (IconInfo icon in p.Icons)
			{
				string text = SafeName(icon.Key);
				if (!IconKeys.Contains(text) && hashSet.Contains(text) && Protocol.TryReadPngSize(icon.Png, out var width2, out var height2) && width2 <= 128 && height2 <= 128)
				{
					WriteAtomic(Path.Combine(_dir, "icons", text + ".png"), icon.Png);
					IconKeys.Add(text);
					num++;
				}
			}
			orCreate.cardUpdated = now;
			orCreate.portraitWidth = width;
			orCreate.portraitHeight = height;
			orCreate.model = p.Model;
			orCreate.hair = p.Hair;
			orCreate.beard = p.Beard;
			orCreate.skinColor = Hex(p.SkinColor);
			orCreate.hairColor = Hex(p.HairColor);
			orCreate.items = p.Items.Select((ItemInfo i) => new ItemRecord
			{
				slot = i.Slot,
				prefab = i.Prefab,
				name = i.Name,
				token = i.Token,
				icon = IconKey(i.Prefab, i.Variant),
				quality = i.Quality,
				variant = i.Variant
			}).ToList();
			orCreate.skills = p.Skills.Select((SkillInfo s) => new SkillRecord
			{
				id = s.Id,
				name = s.Name,
				level = s.Level
			}).ToList();
			orCreate.stats = p.Stats.Select((StatInfo s) => new StatRecord
			{
				id = s.Id,
				value = s.Value
			}).ToList();
			Save(orCreate);
			_dirty = true;
			Plugin.Log.LogInfo((object)$"Saved card of {playerName}: portrait {p.Png.Length / 1024} KB {width}x{height}, {p.Items.Count} items, {p.Skills.Count} skills, {p.Stats.Count} stats, {num} new icons.");
		}

		public static string IconKey(string prefab, int variant)
		{
			return SafeName((variant != 0) ? $"{prefab}-{variant}" : prefab);
		}

		private static PlayerRecord GetOrCreate(string name)
		{
			string text = SafeName(name);
			if (!Players.TryGetValue(text, out var value))
			{
				value = new PlayerRecord
				{
					id = text
				};
				Players[text] = value;
			}
			value.name = name;
			return value;
		}

		private static void Save(PlayerRecord rec)
		{
			try
			{
				WriteAtomic(Path.Combine(_dir, "players", rec.id + ".json"), Encoding.UTF8.GetBytes(Json.Write(rec.ToJson(), pretty: true)));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Cannot save " + rec.id + ".json: " + ex.Message));
			}
		}

		private static void Rebuild()
		{
			_dirty = false;
			_nextRefresh = Time.realtimeSinceStartup + 30f;
			try
			{
				Snapshot snapshot = new Snapshot
				{
					Directory = _dir
				};
				HashSet<string> hashSet = new HashSet<string>(from s in Plugin.HiddenPlayers.Value.Split(',')
					select s.Trim() into s
					where s.Length > 0
					select s, StringComparer.OrdinalIgnoreCase);
				long now = Now;
				Features features = new Features
				{
					PlayTime = Plugin.ShowPlayTime.Value,
					Equipment = Plugin.ShowEquipment.Value,
					ItemIcons = (Plugin.ShowEquipment.Value && Plugin.ShowItemIcons.Value),
					Skills = Plugin.ShowSkills.Value,
					Stats = Plugin.ShowStats.Value
				};
				List<string> list = (from s in Plugin.StatList.Value.Split(',')
					select s.Trim() into s
					where s.Length > 0
					select s).ToList();
				Dictionary<string, long> dictionary = new Dictionary<string, long>();
				foreach (Online value2 in OnlineByPeer.Values)
				{
					dictionary[value2.Id] = value2.Start;
				}
				List<object> list2 = new List<object>();
				int num = 0;
				foreach (PlayerRecord item in Players.Values.OrderBy<PlayerRecord, string>((PlayerRecord r) => r.name, StringComparer.OrdinalIgnoreCase))
				{
					if (!hashSet.Contains(item.name))
					{
						bool flag = item.cardUpdated > 0 && File.Exists(Path.Combine(_dir, "portraits", item.id + ".png"));
						if (flag)
						{
							snapshot.Portraits.Add(item.id);
						}
						if (features.PlayTime && dictionary.ContainsKey(item.id))
						{
							num++;
						}
						Dictionary<string, object> dictionary2 = ToApi(item, features, list, dictionary, now, flag);
						list2.Add(dictionary2);
						snapshot.Json["api/players/" + item.id + ".json"] = Encoding.UTF8.GetBytes(Json.Write(dictionary2));
					}
				}
				Dictionary<string, object> value = new Dictionary<string, object>
				{
					["title"] = (string.IsNullOrWhiteSpace(Plugin.PageTitle.Value) ? ServerName() : Plugin.PageTitle.Value.Trim()),
					["world"] = ((ZNet.World != null) ? ZNet.World.m_name : ""),
					["version"] = "0.2.1",
					["language"] = Plugin.PageLanguage.Value,
					["now"] = now,
					["online"] = num,
					["features"] = new Dictionary<string, object>
					{
						["playTime"] = features.PlayTime,
						["equipment"] = features.Equipment,
						["itemIcons"] = features.ItemIcons,
						["skills"] = features.Skills,
						["stats"] = features.Stats
					},
					["statList"] = list
				};
				snapshot.Json["api/players.json"] = Encoding.UTF8.GetBytes(Json.Write(new Dictionary<string, object>
				{
					["server"] = value,
					["players"] = list2
				}));
				if (features.ItemIcons)
				{
					foreach (string iconKey in IconKeys)
					{
						snapshot.Icons.Add(iconKey);
					}
				}
				Current = snapshot;
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Cannot build the API data: {arg}");
			}
		}

		private static Dictionary<string, object> ToApi(PlayerRecord rec, Features f, List<string> statList, Dictionary<string, long> onlineSince, long now, bool hasPortrait)
		{
			long value;
			bool flag = f.PlayTime && onlineSince.TryGetValue(rec.id, out value) && value > 0;
			long num = (flag ? onlineSince[rec.id] : 0);
			List<object> list = new List<object>();
			if (f.Equipment)
			{
				foreach (ItemRecord item in rec.items)
				{
					list.Add(new Dictionary<string, object>
					{
						["slot"] = item.slot,
						["prefab"] = item.prefab,
						["name"] = item.name,
						["icon"] = ((f.ItemIcons && IconKeys.Contains(item.icon)) ? ("icons/" + item.icon + ".png") : ""),
						["quality"] = item.quality,
						["variant"] = item.variant
					});
				}
			}
			List<object> list2 = new List<object>();
			if (f.Skills)
			{
				foreach (SkillRecord item2 in rec.skills.OrderByDescending((SkillRecord k) => k.level))
				{
					list2.Add(new Dictionary<string, object>
					{
						["id"] = item2.id,
						["name"] = item2.name,
						["level"] = item2.level
					});
				}
			}
			List<object> list3 = new List<object>();
			if (f.Stats)
			{
				Dictionary<string, float> dictionary = new Dictionary<string, float>();
				foreach (StatRecord stat in rec.stats)
				{
					dictionary[stat.id] = stat.value;
				}
				foreach (string stat2 in statList)
				{
					if (dictionary.TryGetValue(stat2, out var value2))
					{
						list3.Add(new Dictionary<string, object>
						{
							["id"] = stat2,
							["value"] = value2
						});
					}
				}
			}
			List<object> list4 = new List<object>();
			if (f.PlayTime)
			{
				foreach (SessionRecord item3 in rec.recent)
				{
					list4.Add(new Dictionary<string, object>
					{
						["start"] = item3.start,
						["end"] = item3.end
					});
				}
			}
			return new Dictionary<string, object>
			{
				["id"] = rec.id,
				["name"] = rec.name,
				["portrait"] = (hasPortrait ? $"portraits/{rec.id}.png?v={rec.cardUpdated}" : ""),
				["portraitWidth"] = (hasPortrait ? rec.portraitWidth : 0),
				["portraitHeight"] = (hasPortrait ? rec.portraitHeight : 0),
				["cardUpdated"] = rec.cardUpdated,
				["online"] = flag,
				["onlineSince"] = num,
				["firstSeen"] = (f.PlayTime ? rec.firstSeen : 0),
				["lastSeen"] = ((!f.PlayTime) ? 0 : (flag ? now : rec.lastSeen)),
				["playSeconds"] = (f.PlayTime ? (rec.playSeconds + (double)(flag ? Math.Max(0L, now - num) : 0)) : 0.0),
				["sessions"] = (f.PlayTime ? rec.sessions : 0),
				["recent"] = list4,
				["model"] = rec.model,
				["skinColor"] = rec.skinColor,
				["hairColor"] = rec.hairColor,
				["items"] = list,
				["skills"] = list2,
				["stats"] = list3
			};
		}

		private static string ServerName()
		{
			try
			{
				if (AccessTools.Field(typeof(ZNet), "m_ServerName")?.GetValue(null) is string { Length: >0 } text)
				{
					return text;
				}
			}
			catch (Exception)
			{
			}
			if (ZNet.World == null)
			{
				return "Valheim";
			}
			return ZNet.World.m_name;
		}

		internal static string SafeName(string name)
		{
			string text = (name ?? "").Normalize(NormalizationForm.FormD);
			StringBuilder stringBuilder = new StringBuilder();
			string text2 = text;
			foreach (char c in text2)
			{
				if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
				{
					char c2 = char.ToLowerInvariant(c);
					if ((c2 >= 'a' && c2 <= 'z') || (c2 >= '0' && c2 <= '9') || c2 == '-' || c2 == '_')
					{
						stringBuilder.Append(c2);
					}
					else if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '_')
					{
						stringBuilder.Append('_');
					}
					if (stringBuilder.Length >= 64)
					{
						break;
					}
				}
			}
			string text3 = stringBuilder.ToString().Trim('_');
			if (text3.Length != 0)
			{
				return text3;
			}
			return "player";
		}

		private static void WriteAtomic(string path, byte[] bytes)
		{
			string text = path + ".tmp";
			File.WriteAllBytes(text, bytes);
			if (File.Exists(path))
			{
				File.Delete(path);
			}
			File.Move(text, path);
		}

		private static string Hex(float[] c)
		{
			return $"#{B(c[0]):x2}{B(c[1]):x2}{B(c[2]):x2}";
			static int B(float v)
			{
				return Mathf.Clamp(Mathf.RoundToInt(v * 255f), 0, 255);
			}
		}
	}
	[BepInPlugin("Manke.ValheimPlayerCards", "ValheimPlayerCards", "0.2.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		public const string Guid = "Manke.ValheimPlayerCards";

		public const string Name = "ValheimPlayerCards";

		public const string Version = "0.2.1";

		internal const string DefaultStats = "Deaths,EnemyKills,BossKills,PlayerKills,Builds,Crafts,TreeChops,MineHits,FishCaught,CreatureTamed,FoodEaten,PortalsUsed,DistanceTraveled,DistanceSail,ArrowsShot,Jumps";

		internal static Plugin Instance;

		internal static ManualLogSource Log;

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

		internal static ConfigEntry<bool> ServerEnabled;

		internal static ConfigEntry<string> DataDirectory;

		internal static ConfigEntry<int> MaxUploadKB;

		internal static ConfigEntry<int> ServerMinIntervalSeconds;

		internal static ConfigEntry<string> HiddenPlayers;

		internal static ConfigEntry<bool> WebEnabled;

		internal static ConfigEntry<string> WebHost;

		internal static ConfigEntry<int> WebPort;

		internal static ConfigEntry<string> AllowOrigin;

		internal static ConfigEntry<string> PageLanguage;

		internal static ConfigEntry<string> PageTitle;

		internal static ConfigEntry<string> CustomPageDirectory;

		internal static ConfigEntry<bool> ShowPlayTime;

		internal static ConfigEntry<bool> ShowEquipment;

		internal static ConfigEntry<bool> ShowItemIcons;

		internal static ConfigEntry<bool> ShowSkills;

		internal static ConfigEntry<bool> ShowStats;

		internal static ConfigEntry<string> StatList;

		internal static ConfigEntry<bool> ClientEnabled;

		internal static ConfigEntry<bool> ShareSkills;

		internal static ConfigEntry<bool> ShareStats;

		internal static ConfigEntry<int> MinIntervalSeconds;

		internal static ConfigEntry<int> Width;

		internal static ConfigEntry<int> Height;

		internal static ConfigEntry<int> Supersampling;

		internal static ConfigEntry<int> TargetKB;

		internal static ConfigEntry<bool> SaveLocalCopy;

		internal static ConfigEntry<float> Yaw;

		internal static ConfigEntry<float> Pitch;

		internal static ConfigEntry<float> Ambient;

		internal static ConfigEntry<float> KeyLight;

		internal static ConfigEntry<float> RimLight;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Expected O, but got Unknown
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c0: Expected O, but got Unknown
			//IL_0157: Unknown result type (might be due to invalid IL or missing references)
			//IL_0161: Expected O, but got Unknown
			//IL_0342: Unknown result type (might be due to invalid IL or missing references)
			//IL_034c: Expected O, but got Unknown
			//IL_037f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0389: Expected O, but got Unknown
			//IL_03bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_03c6: Expected O, but got Unknown
			//IL_03ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_03f7: Expected O, but got Unknown
			//IL_0427: Unknown result type (might be due to invalid IL or missing references)
			//IL_0431: Expected O, but got Unknown
			//IL_0484: Unknown result type (might be due to invalid IL or missing references)
			//IL_048e: Expected O, but got Unknown
			//IL_04c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_04cb: Expected O, but got Unknown
			//IL_04fe: Unknown result type (might be due to invalid IL or missing references)
			//IL_0508: Expected O, but got Unknown
			//IL_053b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0545: Expected O, but got Unknown
			//IL_0578: Unknown result type (might be due to invalid IL or missing references)
			//IL_0582: Expected O, but got Unknown
			//IL_058d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0597: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			ServerEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "Enabled", true, "Accept player cards (portrait, equipment, skills, stats) from players. Only used when this game instance is the server.");
			DataDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "DataDirectory", "", "Where cards are stored. Empty = BepInEx/config/ValheimPlayerCards/<world name>. A relative path starts from the Valheim folder.");
			MaxUploadKB = ((BaseUnityPlugin)this).Config.Bind<int>("1 - Server", "MaxUploadKB", 700, new ConfigDescription("Largest upload accepted (portrait, item icons and data).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 2048), Array.Empty<object>()));
			ServerMinIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("1 - Server", "MinIntervalSeconds", 20, new ConfigDescription("Minimum time between two uploads from the same player.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(5, 3600), Array.Empty<object>()));
			HiddenPlayers = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "HiddenPlayers", "", "Comma-separated player names that never appear on the page or in the API.");
			WebEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Web", "Enabled", true, "Serve the player cards page and the JSON API. Only on the server.");
			WebHost = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "Host", "*", "Address to listen on. * = all addresses, 127.0.0.1 = only this machine (for a reverse proxy on the same host).");
			WebPort = ((BaseUnityPlugin)this).Config.Bind<int>("2 - Web", "Port", 3010, new ConfigDescription("TCP port of the web page and API.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 65535), Array.Empty<object>()));
			AllowOrigin = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "AllowOrigin", "*", "Value of the Access-Control-Allow-Origin header, so other websites can read the API. * = any site, empty = only this page.");
			PageLanguage = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "Language", "auto", "Language of the page: auto (from the visitor's browser) or a language code: en, de, fr, es, pt, ru, pl, it.");
			PageTitle = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "Title", "", "Title shown on the page. Empty = the server name.");
			CustomPageDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "CustomPageDirectory", "", "Folder with your own index.html, app.js, lang.js or style.css; files found there replace the built-in ones.");
			ShowPlayTime = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowPlayTime", true, "Online now, time played, sessions, first and last seen. Tracked by the server.");
			ShowEquipment = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowEquipment", true, "List of the equipped items.");
			ShowItemIcons = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowItemIcons", true, "Game icons next to the equipped items.");
			ShowSkills = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowSkills", true, "Skill levels.");
			ShowStats = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowStats", true, "Character statistics from the game (deaths, kills, builds...). They count all worlds the character has played.");
			StatList = ((BaseUnityPlugin)this).Config.Bind<string>("3 - Card", "StatList", "Deaths,EnemyKills,BossKills,PlayerKills,Builds,Crafts,TreeChops,MineHits,FishCaught,CreatureTamed,FoodEaten,PortalsUsed,DistanceTraveled,DistanceSail,ArrowsShot,Jumps", "Statistics shown, in this order. Names are Valheim's PlayerStatType values.");
			ClientEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "Enabled", true, "Render your character and send your card to servers that have ValheimPlayerCards.");
			ShareSkills = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "ShareSkills", true, "Send your skill levels with the card.");
			ShareStats = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "ShareStats", true, "Send your character statistics with the card.");
			MinIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "MinIntervalSeconds", 30, new ConfigDescription("Minimum time between two cards. The server can ask for a longer wait.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 3600), Array.Empty<object>()));
			Width = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "Width", 512, new ConfigDescription("Portrait width in pixels.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(128, 1024), Array.Empty<object>()));
			Height = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "Height", 768, new ConfigDescription("Portrait height in pixels.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(128, 1536), Array.Empty<object>()));
			Supersampling = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "Supersampling", 2, new ConfigDescription("Render at this many times the size and scale down, for smoother edges.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 3), Array.Empty<object>()));
			TargetKB = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "TargetKB", 300, new ConfigDescription("If the portrait PNG is larger, it is scaled down until it fits.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(32, 1024), Array.Empty<object>()));
			SaveLocalCopy = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "SaveLocalCopy", true, "Also save the last portrait as BepInEx/ValheimPlayerCards/last-portrait.png on your PC.");
			Yaw = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "Yaw", -5f, new ConfigDescription("Character rotation in degrees. The idle stance already turns the body about 35 degrees, so around -5 gives a 3/4 view.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-180f, 180f), Array.Empty<object>()));
			Pitch = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "Pitch", 0f, new ConfigDescription("Forward tilt in degrees.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-30f, 30f), Array.Empty<object>()));
			Ambient = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "Ambient", 0.5f, new ConfigDescription("Ambient light (0 = black, 1 = white).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1.5f), Array.Empty<object>()));
			KeyLight = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "KeyLight", 0.6f, new ConfigDescription("Light from the upper front side.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
			RimLight = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "RimLight", 0.5f, new ConfigDescription("Light from behind that outlines the character.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
			_harmony = new Harmony("Manke.ValheimPlayerCards");
			_harmony.PatchAll(typeof(Plugin).Assembly);
			if (!IsHeadless)
			{
				CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new CardCommand());
			}
			Log.LogInfo((object)"ValheimPlayerCards 0.2.1 loaded.");
		}

		private void Update()
		{
			ClientSide.Tick();
			PlayerStore.Tick();
		}

		private void OnDestroy()
		{
			ServerSide.Stop();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	internal static class PortraitRenderer
	{
		private sealed class PortraitLighting : IDisposable
		{
			private readonly bool _fog;

			private readonly AmbientMode _ambientMode;

			private readonly Color _ambientLight;

			private readonly float _ambientIntensity;

			private readonly List<Light> _disabled = new List<Light>();

			private readonly List<GameObject> _created = new List<GameObject>();

			public PortraitLighting()
			{
				//IL_0028: 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_0033: 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_0065: Unknown result type (might be due to invalid IL or missing references)
				//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
				//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
				_fog = RenderSettings.fog;
				_ambientMode = RenderSettings.ambientMode;
				_ambientLight = RenderSettings.ambientLight;
				_ambientIntensity = RenderSettings.ambientIntensity;
				RenderSettings.fog = false;
				RenderSettings.ambientMode = (AmbientMode)3;
				float value = Plugin.Ambient.Value;
				RenderSettings.ambientLight = new Color(value, value, value, 1f);
				RenderSettings.ambientIntensity = 1f;
				Light[] array = Object.FindObjectsByType<Light>((FindObjectsSortMode)0);
				foreach (Light val in array)
				{
					if (((Behaviour)val).enabled && (val.cullingMask & int.MinValue) != 0 && val.cullingMask != int.MinValue)
					{
						((Behaviour)val).enabled = false;
						_disabled.Add(val);
					}
				}
				AddLight(Quaternion.Euler(35f, 150f, 0f), Plugin.KeyLight.Value);
				AddLight(Quaternion.Euler(20f, -20f, 0f), Plugin.RimLight.Value);
			}

			private void AddLight(Quaternion rotation, float intensity)
			{
				//IL_000e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0014: Expected O, but got Unknown
				//IL_001a: Unknown result type (might be due to invalid IL or missing references)
				if (!(intensity <= 0f))
				{
					GameObject val = new GameObject("PlayerCardLight");
					val.transform.rotation = rotation;
					Light obj = val.AddComponent<Light>();
					obj.type = (LightType)1;
					obj.intensity = intensity;
					obj.shadows = (LightShadows)0;
					obj.cullingMask = int.MinValue;
					_created.Add(val);
				}
			}

			public void Dispose()
			{
				//IL_007e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0089: Unknown result type (might be due to invalid IL or missing references)
				foreach (GameObject item in _created)
				{
					Object.DestroyImmediate((Object)(object)item);
				}
				foreach (Light item2 in _disabled)
				{
					if ((Object)(object)item2 != (Object)null)
					{
						((Behaviour)item2).enabled = true;
					}
				}
				RenderSettings.fog = _fog;
				RenderSettings.ambientMode = _ambientMode;
				RenderSettings.ambientLight = _ambientLight;
				RenderSettings.ambientIntensity = _ambientIntensity;
			}
		}

		private const int RenderLayer = 31;

		private const float SettleSeconds = 1.5f;

		private const float MaxTextureWait = 5f;

		private const int FastForwardSteps = 40;

		private static readonly MethodInfo SetupVisEquipment = AccessTools.Method(typeof(Humanoid), "SetupVisEquipment", (Type[])null, (Type[])null);

		private static readonly MethodInfo UpdateVisuals = AccessTools.Method(typeof(VisEquipment), "UpdateVisuals", (Type[])null, (Type[])null);

		private static readonly int StateF = Animator.StringToHash("statef");

		private static readonly int StateI = Animator.StringToHash("statei");

		private static readonly (string, bool)[] StandingBools = new(string, bool)[12]
		{
			("wakeup", false),
			("onGround", true),
			("inWater", false),
			("encumbered", false),
			("flying", false),
			("crouching", false),
			("blocking", false),
			("dead", false),
			("falling", false),
			("skating", false),
			("skategliding", false),
			("slipping", false)
		};

		private static readonly string[] StandingFloats = new string[4] { "forward_speed", "sideway_speed", "turn_speed", "tilt" };

		private static readonly int SkinColor = Shader.PropertyToID("_SkinColor");

		public static IEnumerator Render(Player player, int maxBytes, int maxSide, Action<byte[]> done)
		{
			GameObject holder = null;
			byte[] png = null;
			List<Texture2D> textures = null;
			try
			{
				holder = CreateClone(player);
				if ((Object)(object)holder == (Object)null)
				{
					yield break;
				}
				textures = RequestFullTextures(holder);
				float start = Time.realtimeSinceStartup;
				while (Time.realtimeSinceStartup - start < 1.5f || (Time.realtimeSinceStartup - start < 5f && !textures.All(LoadedOrGone)))
				{
					yield return null;
					if ((Object)(object)holder == (Object)null || (Object)(object)player == (Object)null)
					{
						yield break;
					}
				}
				try
				{
					png = Capture(holder, maxBytes, maxSide);
				}
				catch (Exception arg)
				{
					Plugin.Log.LogError((object)$"Could not render the portrait: {arg}");
				}
			}
			finally
			{
				if ((Object)(object)holder != (Object)null)
				{
					Object.Destroy((Object)(object)holder);
				}
				if (textures != null)
				{
					foreach (Texture2D item in textures)
					{
						if ((Object)(object)item != (Object)null)
						{
							item.ClearRequestedMipmapLevel();
						}
					}
				}
				done(png);
			}
		}

		private static List<Texture2D> RequestFullTextures(GameObject root)
		{
			HashSet<Texture2D> hashSet = new HashSet<Texture2D>();
			Renderer[] componentsInChildren = root.GetComponentsInChildren<Renderer>(true);
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				Material[] sharedMaterials = componentsInChildren[i].sharedMaterials;
				foreach (Material val in sharedMaterials)
				{
					if ((Object)(object)val == (Object)null)
					{
						continue;
					}
					int[] texturePropertyNameIDs = val.GetTexturePropertyNameIDs();
					foreach (int num in texturePropertyNameIDs)
					{
						Texture texture = val.GetTexture(num);
						Texture2D val2 = (Texture2D)(object)((texture is Texture2D) ? texture : null);
						if (val2 != null && val2.streamingMipmaps && hashSet.Add(val2))
						{
							val2.requestedMipmapLevel = 0;
						}
					}
				}
			}
			return hashSet.ToList();
		}

		private static bool LoadedOrGone(Texture2D t)
		{
			if (!((Object)(object)t == (Object)null))
			{
				return t.IsRequestedMipmapLevelLoaded();
			}
			return true;
		}

		private static GameObject CreateClone(Player player)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Expected O, but got Unknown
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_0139: Unknown result type (might be due to invalid IL or missing references)
			//IL_014b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0150: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = null;
			try
			{
				GameObject val2 = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab("Player") : null);
				if ((Object)(object)val2 == (Object)null)
				{
					Plugin.Log.LogWarning((object)"Player prefab not found, no portrait.");
					return null;
				}
				val = new GameObject("PlayerCardPortrait");
				val.SetActive(false);
				val.transform.position = new Vector3(0f, 6000f, 0f);
				GameObject val3 = Object.Instantiate<GameObject>(val2, val.transform, false);
				((Object)val3).name = "PlayerCardClone";
				StripComponents(val3);
				SetLayer(val3.transform, 31);
				ZNetView.m_forceDisableInit = true;
				try
				{
					val.SetActive(true);
				}
				finally
				{
					ZNetView.m_forceDisableInit = false;
				}
				VisEquipment component = val3.GetComponent<VisEquipment>();
				if ((Object)(object)component == (Object)null)
				{
					Plugin.Log.LogWarning((object)"Player prefab has no VisEquipment, no portrait.");
					Object.Destroy((Object)(object)val);
					return null;
				}
				SetupVisEquipment.Invoke(player, new object[2] { component, false });
				ZNetView component2 = ((Component)player).GetComponent<ZNetView>();
				ZDO val4 = ((component2 != null) ? component2.GetZDO() : null);
				if (val4 != null)
				{
					component.SetModel(val4.GetInt(ZDOVars.s_modelIndex, 0));
					component.SetSkinColor(val4.GetVec3(ZDOVars.s_skinColor, Vector3.one));
					component.SetHairColor(val4.GetVec3(ZDOVars.s_hairColor, Vector3.one));
				}
				UpdateVisuals.Invoke(component, null);
				SetLayer(val3.transform, 31);
				PoseStanding(val3, ((Component)player).GetComponentInChildren<Animator>(), 40);
				return val;
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"Could not build the character copy: {arg}");
				if ((Object)(object)val != (Object)null)
				{
					Object.Destroy((Object)(object)val);
				}
				return null;
			}
		}

		private static void PoseStanding(GameObject clone, Animator source, int steps)
		{
			Animator[] componentsInChildren = clone.GetComponentsInChildren<Animator>();
			Animator[] array = componentsInChildren;
			foreach (Animator val in array)
			{
				val.cullingMode = (AnimatorCullingMode)0;
				val.updateMode = (AnimatorUpdateMode)0;
				val.applyRootMotion = false;
				(string, bool)[] standingBools = StandingBools;
				for (int j = 0; j < standingBools.Length; j++)
				{
					var (text, flag) = standingBools[j];
					if (HasParameter(val, Animator.StringToHash(text), (AnimatorControllerParameterType)4))
					{
						val.SetBool(text, flag);
					}
				}
				string[] standingFloats = StandingFloats;
				foreach (string text2 in standingFloats)
				{
					if (HasParameter(val, Animator.StringToHash(text2), (AnimatorControllerParameterType)1))
					{
						val.SetFloat(text2, 0f);
					}
				}
				if ((Object)(object)source != (Object)null && HasParameter(source, StateF, (AnimatorControllerParameterType)1) && HasParameter(val, StateF, (AnimatorControllerParameterType)1))
				{
					val.SetFloat(StateF, source.GetFloat(StateF));
				}
				if ((Object)(object)source != (Object)null && HasParameter(source, StateI, (AnimatorControllerParameterType)3) && HasParameter(val, StateI, (AnimatorControllerParameterType)3))
				{
					val.SetInteger(StateI, source.GetInteger(StateI));
				}
			}
			for (int k = 0; k < steps; k++)
			{
				array = componentsInChildren;
				for (int i = 0; i < array.Length; i++)
				{
					array[i].Update(0.2f);
				}
			}
		}

		private static bool HasParameter(Animator anim, int hash, AnimatorControllerParameterType type)
		{
			//IL_000e: 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)
			return anim.parameters.Any((AnimatorControllerParameter p) => p.nameHash == hash && p.type == type);
		}

		private static bool Keep(Component c)
		{
			if (c is Transform || c is Renderer || c is MeshFilter || c is Animator || c is LODGroup || c is VisEquipment || c is ZNetView || c is ParticleSystem)
			{
				return true;
			}
			return (((object)c).GetType().Namespace ?? "").StartsWith("MagicaCloth", StringComparison.Ordinal);
		}

		private static void StripComponents(GameObject root)
		{
			Func<Component, int>[] array = new Func<Component, int>[4]
			{
				(Component c) => (!(c is MonoBehaviour)) ? 99 : 0,
				(Component c) => (!((object)c).GetType().Name.Contains("Joint")) ? 99 : 0,
				(Component c) => 0,
				(Component c) => 0
			};
			foreach (Func<Component, int> func in array)
			{
				Component[] componentsInChildren = root.GetComponentsInChildren<Component>(true);
				foreach (Component val in componentsInChildren)
				{
					if (!((Object)(object)val == (Object)null) && !Keep(val) && func(val) == 0)
					{
						try
						{
							Object.DestroyImmediate((Object)(object)val);
						}
						catch (Exception)
						{
						}
					}
				}
			}
		}

		private static void SetLayer(Transform t, int layer)
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Expected O, but got Unknown
			((Component)t).gameObject.layer = layer;
			foreach (Transform item in t)
			{
				SetLayer(item, layer);
			}
		}

		private static void DisableLowerLods(GameObject root)
		{
			LODGroup[] componentsInChildren = root.GetComponentsInChildren<LODGroup>(true);
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				LOD[] lODs = componentsInChildren[i].GetLODs();
				if (lODs.Length < 2)
				{
					continue;
				}
				HashSet<Renderer> hashSet = new HashSet<Renderer>(lODs[0].renderers.Where((Renderer r) => (Object)(object)r != (Object)null));
				for (int num = 1; num < lODs.Length; num++)
				{
					Renderer[] renderers = lODs[num].renderers;
					foreach (Renderer val in renderers)
					{
						if ((Object)(object)val != (Object)null && !hashSet.Contains(val))
						{
							val.enabled = false;
						}
					}
				}
			}
		}

		private static void BakePropertyBlocks(GameObject root)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			//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_0072: Unknown result type (might be due to invalid IL or missing references)
			MaterialPropertyBlock val = new MaterialPropertyBlock();
			Renderer[] componentsInChildren = root.GetComponentsInChildren<Renderer>(true);
			foreach (Renderer val2 in componentsInChildren)
			{
				if (!val2.HasPropertyBlock())
				{
					continue;
				}
				val2.GetPropertyBlock(val);
				if (!val.HasColor(SkinColor))
				{
					continue;
				}
				Color color = val.GetColor(SkinColor);
				Material[] materials = val2.materials;
				foreach (Material val3 in materials)
				{
					if ((Object)(object)val3 != (Object)null && val3.HasProperty(SkinColor))
					{
						val3.SetColor(SkinColor, color);
					}
				}
			}
		}

		private static byte[] Capture(GameObject holder, int maxBytes, int maxSide)
		{
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: 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_0083: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b3: Expected O, but got Unknown
			GameObject gameObject = ((Component)holder.transform.GetChild(0)).gameObject;
			DisableLowerLods(gameObject);
			BakePropertyBlocks(gameObject);
			PoseStanding(gameObject, ((Object)(object)Player.m_localPlayer != (Object)null) ? ((Component)Player.m_localPlayer).GetComponentInChildren<Animator>() : null, 10);
			int value = Plugin.Supersampling.Value;
			int num = Mathf.Min(Plugin.Width.Value, maxSide);
			int num2 = Mathf.Min(Plugin.Height.Value, maxSide);
			RenderRequest val = new RenderRequest(gameObject)
			{
				Width = num * value,
				Height = num2 * value,
				Rotation = Quaternion.Euler(Plugin.Pitch.Value, Plugin.Yaw.Value, 0f),
				ParticleSimulationTime = 1f
			};
			Sprite val2 = null;
			using (new PortraitLighting())
			{
				val2 = RenderManager.Instance.Render(val);
			}
			if ((Object)(object)val2 == (Object)null || (Object)(object)val2.texture == (Object)null)
			{
				Plugin.Log.LogWarning((object)"The renderer returned nothing.");
				return null;
			}
			Texture2D texture = val2.texture;
			Color32[] pixels;
			try
			{
				pixels = texture.GetPixels32();
			}
			finally
			{
				Object.Destroy((Object)(object)val2);
				Object.Destroy((Object)(object)texture);
			}
			float num3 = 1f;
			for (int i = 0; i < 6; i++)
			{
				int num4 = Mathf.Max(64, Mathf.RoundToInt((float)num * num3));
				int num5 = Mathf.Max(64, Mathf.RoundToInt((float)num2 * num3));
				byte[] array = Encode(Resample(pixels, num * value, num2 * value, num4, num5), num4, num5);
				if (array.Length <= maxBytes)
				{
					return array;
				}
				Plugin.Log.LogInfo((object)$"Portrait {num4}x{num5} is {array.Length / 1024} KB, trying smaller.");
				num3 *= 0.8f;
			}
			Plugin.Log.LogWarning((object)"Could not make the portrait small enough, not sent.");
			return null;
		}

		private static byte[] Encode(Color32[] pixels, int w, int h)
		{
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Expected O, but got Unknown
			Texture2D val = new Texture2D(w, h, (TextureFormat)4, false);
			try
			{
				val.SetPixels32(pixels);
				val.Apply(false);
				return ImageConversion.EncodeToPNG(val);
			}
			finally
			{
				Object.Destroy((Object)(object)val);
			}
		}

		private static Color32[] Resample(Color32[] src, int sw, int sh, int dw, int dh)
		{
			//IL_0124: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: 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_008b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_0129: Unknown result type (might be due to invalid IL or missing references)
			if (sw == dw && sh == dh)
			{
				return src;
			}
			Color32[] array = (Color32[])(object)new Color32[dw * dh];
			for (int i = 0; i < dh; i++)
			{
				int num = i * sh / dh;
				int num2 = Math.Max(num + 1, (i + 1) * sh / dh);
				for (int j = 0; j < dw; j++)
				{
					int num3 = j * sw / dw;
					int num4 = Math.Max(num3 + 1, (j + 1) * sw / dw);
					long num5 = 0L;
					long num6 = 0L;
					long num7 = 0L;
					long num8 = 0L;
					long num9 = 0L;
					for (int k = num; k < num2; k++)
					{
						int num10 = k * sw;
						for (int l = num3; l < num4; l++)
						{
							Color32 val = src[num10 + l];
							num5 += val.r * val.a;
							num6 += val.g * val.a;
							num7 += val.b * val.a;
							num8 += val.a;
							num9++;
						}
					}
					array[i * dw + j] = ((num8 == 0L) ? new Color32((byte)0, (byte)0, (byte)0, (byte)0) : new Color32((byte)(num5 / num8), (byte)(num6 / num8), (byte)(num7 / num8), (byte)(num8 / num9)));
				}
			}
			return array;
		}
	}
	internal sealed class ItemInfo
	{
		public string Slot = "";

		public string Prefab = "";

		public string Name = "";

		public string Token = "";

		public int Quality;

		public int Variant;
	}
	internal sealed class SkillInfo
	{
		public string Id = "";

		public string Name = "";

		public float Level;
	}
	internal sealed class StatInfo
	{
		public string Id = "";

		public float Value;
	}
	internal sealed class IconInfo
	{
		public string Key = "";

		public byte[] Png = Array.Empty<byte>();
	}
	internal sealed class CardPayload
	{
		public readonly List<ItemInfo> Items = new List<ItemInfo>();

		public readonly List<SkillInfo> Skills = new List<SkillInfo>();

		public readonly List<StatInfo> Stats = new List<StatInfo>();

		public readonly List<IconInfo> Icons = new List<IconInfo>();

		public int Model;

		public string Hair = "";

		public string Beard = "";

		public float[] SkinColor = new float[3] { 1f, 1f, 1f };

		public float[] HairColor = new float[3] { 1f, 1f, 1f };

		public byte[] Png = Array.Empty<byte>();
	}
	internal static class Protocol
	{
		public const int Version = 1;

		public const string RpcHello = "ValheimPlayerCards_Hello";

		public const string RpcReady = "ValheimPlayerCards_Ready";

		public const string RpcUpload = "ValheimPlayerCards_Upload";

		public const int ChunkSize = 60000;

		public const int HardMaxBytes = 2097152;

		public const int MaxItems = 24;

		public const int MaxSkills = 96;

		public const int MaxStats = 512;

		public const int MaxIcons = 24;

		public const int MaxIconBytes = 49152;

		public const int MaxIconSide = 128;

		public const int MaxKnownIcons = 2000;

		public const int MaxStringLength = 128;

		public const int MaxImageSide = 2048;

		private static readonly byte[] PngSignature = new byte[8] { 137, 80, 78, 71, 13, 10, 26, 10 };

		public static int ChunkCount(int total)
		{
			return (total + 60000 - 1) / 60000;
		}

		public static byte[] Serialize(CardPayload p)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
			{
				binaryWriter.Write(1);
				int num = Math.Min(p.Items.Count, 24);
				binaryWriter.Write(num);
				for (int i = 0; i < num; i++)
				{
					ItemInfo itemInfo = p.Items[i];
					binaryWriter.Write(Clip(itemInfo.Slot));
					binaryWriter.Write(Clip(itemInfo.Prefab));
					binaryWriter.Write(Clip(itemInfo.Name));
					binaryWriter.Write(Clip(itemInfo.Token));
					binaryWriter.Write(itemInfo.Quality);
					binaryWriter.Write(itemInfo.Variant);
				}
				binaryWriter.Write(p.Model);
				binaryWriter.Write(Clip(p.Hair));
				binaryWriter.Write(Clip(p.Beard));
				for (int j = 0; j < 3; j++)
				{
					binaryWriter.Write(p.SkinColor[j]);
				}
				for (int k = 0; k < 3; k++)
				{
					binaryWriter.Write(p.HairColor[k]);
				}
				int num2 = Math.Min(p.Skills.Count, 96);
				binaryWriter.Write(num2);
				for (int l = 0; l < num2; l++)
				{
					binaryWriter.Write(Clip(p.Skills[l].Id));
					binaryWriter.Write(Clip(p.Skills[l].Name));
					binaryWriter.Write(p.Skills[l].Level);
				}
				int num3 = Math.Min(p.Stats.Count, 512);
				binaryWriter.Write(num3);
				for (int m = 0; m < num3; m++)
				{
					binaryWriter.Write(Clip(p.Stats[m].Id));
					binaryWriter.Write(p.Stats[m].Value);
				}
				int num4 = Math.Min(p.Icons.Count, 24);
				binaryWriter.Write(num4);
				for (int n = 0; n < num4; n++)
				{
					binaryWriter.Write(Clip(p.Icons[n].Key));
					binaryWriter.Write(p.Icons[n].Png.Length);
					binaryWriter.Write(p.Icons[n].Png);
				}
				binaryWriter.Write(p.Png.Length);
				binaryWriter.Write(p.Png);
			}
			return memoryStream.ToArray();
		}

		public static CardPayload Deserialize(byte[] data)
		{
			using BinaryReader binaryReader = new BinaryReader(new MemoryStream(data, writable: false), Encoding.UTF8);
			if (binaryReader.ReadInt32() != 1)
			{
				throw new InvalidDataException("unknown payload version");
			}
			CardPayload cardPayload = new CardPayload();
			int num = ReadCount(binaryReader, 24, "items");
			for (int i = 0; i < num; i++)
			{
				cardPayload.Items.Add(new ItemInfo
				{
					Slot = ReadString(binaryReader),
					Prefab = ReadString(binaryReader),
					Name = ReadString(binaryReader),
					Token = ReadString(binaryReader),
					Quality = binaryReader.ReadInt32(),
					Variant = binaryReader.ReadInt32()
				});
			}
			cardPayload.Model = binaryReader.ReadInt32();
			cardPayload.Hair = ReadString(binaryReader);
			cardPayload.Beard = ReadString(binaryReader);
			for (int j = 0; j < 3; j++)
			{
				cardPayload.SkinColor[j] = binaryReader.ReadSingle();
			}
			for (int k = 0; k < 3; k++)
			{
				cardPayload.HairColor[k] = binaryReader.ReadSingle();
			}
			int num2 = ReadCount(binaryReader, 96, "skills");
			for (int l = 0; l < num2; l++)
			{
				cardPayload.Skills.Add(new SkillInfo
				{
					Id = ReadString(binaryReader),
					Name = ReadString(binaryReader),
					Level = Finite(binaryReader.ReadSingle())
				});
			}
			int num3 = ReadCount(binaryReader, 512, "stats");
			for (int m = 0; m < num3; m++)
			{
				cardPayload.Stats.Add(new StatInfo
				{
					Id = ReadString(binaryReader),
					Value = Finite(binaryReader.ReadSingle())
				});
			}
			int num4 = ReadCount(binaryReader, 24, "icons");
			for (int n = 0; n < num4; n++)
			{
				string key = ReadString(binaryReader);
				cardPayload.Icons.Add(new IconInfo
				{
					Key = key,
					Png = ReadBytes(binaryReader, 49152)
				});
			}
			cardPayload.Png = ReadBytes(binaryReader, data.Length);
			return cardPayload;
		}

		public static bool TryReadPngSize(byte[] png, out int width, out int height)
		{
			width = (height = 0);
			if (png == null || png.Length < 33)
			{
				return false;
			}
			for (int i = 0; i < PngSignature.Length; i++)
			{
				if (png[i] != PngSignature[i])
				{
					return false;
				}
			}
			if (png[12] != 73 || png[13] != 72 || png[14] != 68 || png[15] != 82)
			{
				return false;
			}
			width = (png[16] << 24) | (png[17] << 16) | (png[18] << 8) | png[19];
			height = (png[20] << 24) | (png[21] << 16) | (png[22] << 8) | png[23];
			if (width > 0)
			{
				return height > 0;
			}
			return false;
		}

		private static int ReadCount(BinaryReader r, int max, string what)
		{
			int num = r.ReadInt32();
			if (num < 0 || num > max)
			{
				throw new InvalidDataException("too many " + what);
			}
			return num;
		}

		private static byte[] ReadBytes(BinaryReader r, int max)
		{
			int num = r.ReadInt32();
			if (num <= 0 || num > max)
			{
				throw new InvalidDataException("bad image length");
			}
			byte[] array = r.ReadBytes(num);
			if (array.Length != num)
			{
				throw new InvalidDataException("truncated image");
			}
			return array;
		}

		private static float Finite(float v)
		{
			if (!float.IsNaN(v) && !float.IsInfinity(v))
			{
				return v;
			}
			return 0f;
		}

		private static string ReadString(BinaryReader r)
		{
			string text = r.ReadString();
			if (text.Length > 128)
			{
				throw new InvalidDataException("string too long");
			}
			StringBuilder stringBuilder = new StringBuilder(text.Length);
			string text2 = text;
			foreach (char c in text2)
			{
				if (!char.IsControl(c))
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}

		private static string Clip(string s)
		{
			s = s ?? "";
			if (s.Length <= 128)
			{
				return s;
			}
			return s.Substring(0, 128);
		}
	}
	internal static class ServerSide
	{
		private sealed class Upload
		{
			public int Id;

			public int Count;

			public int Next;

			public byte[] Buffer;

			public int Written;

			public float Started;
		}

		private static readonly Dictionary<long, Upload> Uploads = new Dictionary<long, Upload>();

		private static readonly Dictionary<string, float> LastStart = new Dictionary<string, float>();

		private static WebServer _web;

		private static int MaxBytes => Plugin.MaxUploadKB.Value * 1024;

		public static void Start()
		{
			if (Plugin.ServerEnabled.Value)
			{
				try
				{
					PlayerStore.Start();
				}
				catch (Exception arg)
				{
					Plugin.Log.LogError((object)$"Cannot open the player cards folder: {arg}");
					return;
				}
				if (Plugin.WebEnabled.Value)
				{
					_web = new WebServer();
					_web.Start(Plugin.WebHost.Value, Plugin.WebPort.Value);
				}
			}
		}

		public static void Stop()
		{
			_web?.Stop();
			_web = null;
			PlayerStore.Stop();
			Uploads.Clear();
			LastStart.Clear();
		}

		public static void RegisterPeer(ZNetPeer peer)
		{
			peer.m_rpc.Register<ZPackage>("ValheimPlayerCards_Hello", (Action<ZRpc, ZPackage>)OnHello);
			peer.m_rpc.Register<ZPackage>("ValheimPlayerCards_Upload", (Action<ZRpc, ZPackage>)OnUpload);
		}

		public static void OnPeerLeft(ZNetPeer peer)
		{
			if (peer != null)
			{
				Uploads.Remove(peer.m_uid);
				PlayerStore.OnPeerLeft(peer);
			}
		}

		private static ZNetPeer FindPeer(ZRpc rpc)
		{
			ZNet instance = ZNet.instance;
			if (instance == null)
			{
				return null;
			}
			return ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_rpc == rpc));
		}

		private static void OnHello(ZRpc rpc, ZPackage pkg)
		{
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Expected O, but got Unknown
			try
			{
				if (!PlayerStore.Active)
				{
					return;
				}
				int num = pkg.ReadInt();
				ZNetPeer val = FindPeer(rpc);
				string text = ((val != null && !string.IsNullOrEmpty(val.m_playerName)) ? val.m_playerName : "a player");
				if (num != 1)
				{
					Plugin.Log.LogWarning((object)$"{text} uses ValheimPlayerCards protocol {num}, this server uses {1}.");
					return;
				}
				ZPackage val2 = new ZPackage();
				val2.Write(1);
				val2.Write(MaxBytes);
				val2.Write(Plugin.ServerMinIntervalSeconds.Value);
				val2.Write(2048);
				List<string> list = PlayerStore.KnownIcons.Take(2000).ToList();
				val2.Write(list.Count);
				foreach (string item in list)
				{
					val2.Write(item);
				}
				rpc.Invoke("ValheimPlayerCards_Ready", new object[1] { val2 });
				Plugin.Log.LogInfo((object)(text + " has ValheimPlayerCards."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Bad hello: " + ex.Message));
			}
		}

		private static void OnUpload(ZRpc rpc, ZPackage pkg)
		{
			ZNetPeer val = FindPeer(rpc);
			if (val == null || !PlayerStore.Active)
			{
				return;
			}
			try
			{
				if (pkg.ReadInt() != 1)
				{
					return;
				}
				int num = pkg.ReadInt();
				int num2 = pkg.ReadInt();
				int num3 = pkg.ReadInt();
				int num4 = pkg.ReadInt();
				byte[] array = pkg.ReadByteArray();
				if (num2 == 0)
				{
					Uploads.Remove(val.m_uid);
					if (string.IsNullOrEmpty(val.m_playerName))
					{
						Plugin.Log.LogWarning((object)"Card from a connection without a player name, ignored.");
						return;
					}
					if (num4 <= 0 || num4 > MaxBytes)
					{
						Plugin.Log.LogWarning((object)$"Card from {val.m_playerName} is {num4 / 1024} KB, limit is {Plugin.MaxUploadKB.Value} KB. Ignored.");
						return;
					}
					if (num3 != Protocol.ChunkCount(num4))
					{
						return;
					}
					string key = PlayerStore.SafeName(val.m_playerName);
					float realtimeSinceStartup = Time.realtimeSinceStartup;
					if (LastStart.TryGetValue(key, out var value) && realtimeSinceStartup - value < (float)Plugin.ServerMinIntervalSeconds.Value)
					{
						Plugin.Log.LogInfo((object)("Card from " + val.m_playerName + " came too soon after the previous one, ignored."));
						return;
					}
					LastStart[key] = realtimeSinceStartup;
					Uploads[val.m_uid] = new Upload
					{
						Id = num,
						Count = num3,
						Buffer = new byte[num4],
						Started = realtimeSinceStartup
					};
				}
				if (!Uploads.TryGetValue(val.m_uid, out var value2) || value2.Id != num)
				{
					return;
				}
				bool flag = num2 == value2.Count - 1;
				int num5 = (flag ? (value2.Buffer.Length - value2.Written) : 60000);
				if (num2 != value2.Next || array.Length != num5 || Time.realtimeSinceStartup - value2.Started > 120f)
				{
					Uploads.Remove(val.m_uid);
					Plugin.Log.LogWarning((object)("Card from " + val.m_playerName + " arrived broken, ignored."));
					return;
				}
				Buffer.BlockCopy(array, 0, value2.Buffer, value2.Written, array.Length);
				value2.Written += array.Length;
				value2.Next++;
				if (flag)
				{
					Uploads.Remove(val.m_uid);
					Save(val.m_playerName, value2.Buffer);
				}
			}
			catch (Exception ex)
			{
				Uploads.Remove(val.m_uid);
				Plugin.Log.LogWarning((object)("Bad card from " + val.m_playerName + ": " + ex.Message));
			}
		}

		public static void Save(string playerName, byte[] payload)
		{
			if (PlayerStore.Active)
			{
				CardPayload cardPayload;
				try
				{
					cardPayload = Protocol.Deserialize(payload);
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("Card from " + playerName + " could not be read: " + ex.Message));
					return;
				}
				if (!Protocol.TryReadPngSize(cardPayload.Png, out var width, out var height) || width > 2048 || height > 2048)
				{
					Plugin.Log.LogWarning((object)("Portrait from " + playerName + " is not a valid PNG, ignored."));
				}
				else
				{
					PlayerStore.SaveCard(playerName, cardPayload, width, height);
				}
			}
		}
	}
	internal sealed class WebServer
	{
		private const int MaxConnections = 32;

		private const int MaxRequestBytes = 8192;

		private static readonly Dictionary<string, string> PageFiles = new Dictionary<string, string>
		{
			{ "index.html", "text/html; charset=utf-8" },
			{ "app.js", "text/javascript; charset=utf-8" },
			{ "lang.js", "text/javascript; charset=utf-8" },
			{ "style.css", "text/css; charset=utf-8" }
		};

		private readonly Dictionary<string, byte[]> _builtIn = new Dictionary<string, byte[]>();

		private TcpListener _listener;

		private Thread _thread;

		private volatile bool _running;

		private int _connections;

		private string _allowOrigin;

		private string _customDir;

		public void Start(string host, int port)
		{
			foreach (string key in PageFiles.Keys)
			{
				using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("web/" + key);
				if (stream != null)
				{
					MemoryStream memoryStream = new MemoryStream();
					stream.CopyTo(memoryStream);
					_builtIn[key] = memoryStream.ToArray();
				}
			}
			_allowOrigin = (Plugin.AllowOrigin.Value ?? "").Trim();
			_customDir = (Plugin.CustomPageDirectory.Value ?? "").Trim();
			IPAddress address = IPAddress.Any;
			if (!string.IsNullOrWhiteSpace(host) && host.Trim() != "*" && !IPAddress.TryParse(host.Trim(), out address))
			{
				Plugin.Log.LogError((object)("Web Host '" + host + "' is not an IP address; the page is not served."));
				return;
			}
			try
			{
				_listener = new TcpListener(address, port);
				_listener.Start();
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)$"Cannot listen on {address}:{port} ({ex.Message}); the page is not served.");
				_listener = null;
				return;
			}
			_running = true;
			_thread = new Thread(AcceptLoop)
			{
				IsBackground = true,
				Name = "ValheimPlayerCards web"
			};
			_thread.Start();
			Plugin.Log.LogInfo((object)string.Format("Player cards page on http://{0}:{1}/", address.Equals(IPAddress.Any) ? "*" : address.ToString(), port));
		}

		public void Stop()
		{
			_running = false;
			try
			{
				_listener?.Stop();
			}
			catch (Exception)
			{
			}
			_listener = null;
		}

		private void AcceptLoop()
		{
			while (_running)
			{
				TcpClient client;
				try
				{
					client = _listener.AcceptTcpClient();
				}
				catch (Exception)
				{
					if (!_running)
					{
						break;
					}
					Thread.Sleep(100);
					continue;
				}
				if (Interlocked.Increment(ref _connections) > 32)
				{
					Interlocked.Decrement(ref _connections);
					client.Close();
					continue;
				}
				ThreadPool.QueueUserWorkItem(delegate
				{
					try
					{
						Handle(client);
					}
					catch (Exception)
					{
					}
					finally
					{
						client.Close();
						Interlocked.Decrement(ref _connections);
					}
				});
			}
		}

		private void Handle(TcpClient client)
		{
			client.ReceiveTimeout = 5000;
			client.SendTimeout = 10000;
			NetworkStream stream = client.GetStream();
			string text = ReadHead(stream);
			if (text == null)
			{
				return;
			}
			string[] array = text.Split(new string[1] { "\r\n" }, StringSplitOptions.None)[0].Split(' ');
			if (array.Length < 3)
			{
				Send(stream, 400, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Bad request"), headOnly: false, null);
				return;
			}
			string text2 = array[0];
			string text3 = array[1];
			bool flag = text2 == "HEAD";
			if (text2 == "OPTIONS")
			{
				Send(stream, 204, null, Array.Empty<byte>(), headOnly: true, null);
				return;
			}
			if (text2 != "GET" && !flag)
			{
				Send(stream, 405, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Method not allowed"), headOnly: false, null);
				return;
			}
			int num = text3.IndexOfAny(new char[2] { '?', '#' });
			string text4 = Uri.UnescapeDataString((num >= 0) ? text3.Substring(0, num) : text3).TrimStart('/');
			if (text4.Length == 0)
			{
				text4 = "index.html";
			}
			Snapshot current = PlayerStore.Current;
			if (current == null)
			{
				Send(stream, 503, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Starting"), flag, "no-store");
				return;
			}
			if (PageFiles.TryGetValue(text4, out var value))
			{
				byte[] value2;
				byte[] array2 = Custom(text4) ?? (_builtIn.TryGetValue(text4, out value2) ? value2 : null);
				if (array2 != null)
				{
					Send(stream, 200, value, array2, flag, "no-cache");
					return;
				}
			}
			else
			{
				if (current.Json.TryGetValue(text4, out var value3))
				{
					Send(stream, 200, "application/json; charset=utf-8", value3, flag, "no-cache");
					return;
				}
				if (TryImage(current, text4, "portraits/", current.Portraits, out var file) || TryImage(current, text4, "icons/", current.Icons, out file))
				{
					byte[] body = File.ReadAllBytes(file);
					Send(stream, 200, "image/png", body, flag, "public, max-age=300");
					return;
				}
			}
			Send(stream, 404, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Not found"), flag, "no-cache");
		}

		private static bool TryImage(Snapshot snap, string path, string prefix, HashSet<string> known, out string file)
		{
			file = null;
			if (!path.StartsWith(prefix, StringComparison.Ordinal) || !path.EndsWith(".png", StringComparison.Ordinal))
			{
				return false;
			}
			string text = path.Substring(prefix.Length, path.Length - prefix.Length - 4);
			if (!known.Contains(text))
			{
				return false;
			}
			file = Path.Combine(snap.Directory, prefix.TrimEnd('/'), text + ".png");
			return File.Exists(file);
		}

		private byte[] Custom(string name)
		{
			if (_customDir.Length == 0)
			{
				return null;
			}
			try
			{
				string path = Path.Combine(_customDir, name);
				return File.Exists(path) ? File.ReadAllBytes(path) : null;
			}
			catch (Exception)
			{
				return null;
			}
		}

		private static string ReadHead(NetworkStream stream)
		{
			byte[] array = new byte[8192];
			int num = 0;
			while (num < array.Length)
			{
				int num2 = stream.Read(array, num, array.Length - num);
				if (num2 <= 0)
				{
					return null;
				}
				num += num2;
				string text = Encoding.ASCII.GetString(array, 0, num);
				int num3 = text.IndexOf("\r\n\r\n", StringComparison.Ordinal);
				if (num3 >= 0)
				{
					return text.Substring(0, num3);
				}
			}
			return null;
		}

		private void Send(NetworkStream stream, int status, string type, byte[] body, bool headOnly, string cache)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("HTTP/1.1 ").Append(status).Append(' ')
				.Append(Reason(status))
				.Append("\r\n");
			if (type != null)
			{
				stringBuilder.Append("Content-Type: ").Append(type).Append("\r\n");
			}
			stringBuilder.Append("Content-Length: ").Append(body.Length).Append("\r\n");
			if (cache != null)
			{
				stringBuilder.Append("Cache-Control: ").Append(cache).Append("\r\n");
			}
			if (_allowOrigin.Length > 0)
			{
				stringBuilder.Append("Access-Control-Allow-Origin: ").Append(_allowOrigin).Append("\r\n");
				stringBuilder.Append("Access-Control-Allow-Methods: GET, HEAD, OPTIONS\r\n");
			}
			stringBuilder.Append("X-Content-Type-Options: nosniff\r\n");
			stringBuilder.Append("Server: ValheimPlayerCards\r\n");
			stringBuilder.Append("Connection: close\r\n\r\n");
			byte[] bytes = Encoding.ASCII.GetBytes(stringBuilder.ToString());
			stream.Write(bytes, 0, bytes.Length);
			if (!headOnly && body.Length != 0)
			{
				stream.Write(body, 0, body.Length);
			}
			stream.Flush();
		}

		private static string Reason(int status)
		{
			return status switch
			{
				200 => "OK", 
				204 => "No Content", 
				40