Decompiled source of FindGameServerMod v0.1.2

FindGameServerMod.dll

Decompiled 6 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Splatform;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("assembly_valheim")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[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 FindGameServerMod
{
	internal static class Commands
	{
		private class Showing
		{
			public long Peer;

			public string Text;

			public float Until;

			public float Next;
		}

		private const int MaxText = 200;

		private const int MaxPerAsk = 20;

		private const float RepeatEverySeconds = 2f;

		private const float HoldSeconds = 8f;

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

		private static readonly Regex PlayerId = new Regex("^(7656119\\d{10}|[A-Za-z][A-Za-z0-9]{1,15}_[0-9A-Za-z-]{4,48})$");

		private static float pollSeconds = 3f;

		private static float lastAskAt = -1000f;

		private static int asking;

		private static int failures;

		private static readonly object Gate = new object();

		private static readonly List<Dictionary<string, object>> Pending = new List<Dictionary<string, object>>();

		private static readonly List<object> Results = new List<object>();

		private static readonly Queue<string> DoneOrder = new Queue<string>();

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

		private static readonly List<Showing> Repeats = new List<Showing>();

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

		private static string Norm(string id)
		{
			string text;
			if (id == null || !id.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase))
			{
				text = id;
				if (text == null)
				{
					return "";
				}
			}
			else
			{
				text = id.Substring(6);
			}
			return text;
		}

		public static string WhyRemoved(string userId)
		{
			string text = Norm(userId);
			if (text == "" || !Removed.TryGetValue(text, out var value))
			{
				return null;
			}
			Removed.Remove(text);
			if (!(Time.realtimeSinceStartup - value.Value < 60f))
			{
				return null;
			}
			return value.Key;
		}

		private static void MarkRemoved(string userId, string why)
		{
			Removed[Norm(userId)] = new KeyValuePair<string, float>(why, Time.realtimeSinceStartup);
		}

		public static void OnReportReply(object reply)
		{
			if (reply is Dictionary<string, object> dictionary && dictionary.TryGetValue("pollSeconds", out var value) && value is double num && num >= 1.0 && num <= 60.0)
			{
				pollSeconds = (float)num;
			}
		}

		public static void Tick(string reportUrl, string token, ManualLogSource log)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (Repeats.Count > 0)
			{
				RepeatNotices(realtimeSinceStartup);
			}
			RunPending(log);
			float num = Math.Min(60f, pollSeconds * (float)(1 << Math.Min(Volatile.Read(in failures), 5)));
			if (realtimeSinceStartup - lastAskAt >= num && Interlocked.CompareExchange(ref asking, 1, 0) == 0)
			{
				lastAskAt = realtimeSinceStartup;
				Ask(reportUrl, token, log);
			}
		}

		private static void Ask(string reportUrl, string token, ManualLogSource log)
		{
			List<object> results;
			lock (Gate)
			{
				results = Results.ToList();
				Results.Clear();
			}
			string target = (reportUrl.EndsWith("/report") ? (reportUrl.Substring(0, reportUrl.Length - "/report".Length) + "/commands") : null);
			if (target == null)
			{
				Interlocked.Exchange(ref asking, 0);
				return;
			}
			string json = Json.Write(new Dictionary<string, object>
			{
				{ "token", token },
				{ "results", results }
			});
			Task.Run(async delegate
			{
				bool delivered = false;
				try
				{
					StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
					try
					{
						HttpResponseMessage res = await Http.PostAsync(target, (HttpContent)(object)content).ConfigureAwait(continueOnCapturedContext: false);
						try
						{
							string text = await res.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);
							delivered = res.IsSuccessStatusCode;
							if (delivered)
							{
								TakeCommands(text);
							}
							else if (res.StatusCode == HttpStatusCode.BadRequest)
							{
								log.LogWarning((object)("FindGameServer turned down a command check: " + text));
							}
						}
						finally
						{
							((IDisposable)res)?.Dispose();
						}
					}
					finally
					{
						((IDisposable)content)?.Dispose();
					}
				}
				catch (Exception)
				{
				}
				finally
				{
					if (delivered)
					{
						Interlocked.Exchange(ref failures, 0);
					}
					else
					{
						Interlocked.Increment(ref failures);
					}
					if (!delivered && results.Count > 0)
					{
						lock (Gate)
						{
							Results.InsertRange(0, results);
						}
					}
					Interlocked.Exchange(ref asking, 0);
				}
			});
		}

		private static void TakeCommands(string body)
		{
			object obj;
			try
			{
				obj = Json.Parse(body);
			}
			catch
			{
				return;
			}
			if (!(obj is Dictionary<string, object> dictionary) || !dictionary.TryGetValue("commands", out var value) || !(value is List<object> source))
			{
				return;
			}
			lock (Gate)
			{
				foreach (object item2 in source.Take(20))
				{
					if (item2 is Dictionary<string, object> item)
					{
						Pending.Add(item);
					}
				}
			}
		}

		private static void RunPending(ManualLogSource log)
		{
			List<Dictionary<string, object>> list;
			lock (Gate)
			{
				if (Pending.Count == 0)
				{
					return;
				}
				list = Pending.ToList();
				Pending.Clear();
			}
			foreach (Dictionary<string, object> item in list)
			{
				string text = Str(item, "id");
				if (!(text == "") && !Done.Contains(text))
				{
					Remember(text);
					bool flag;
					string message;
					try
					{
						flag = Do(item, out message);
					}
					catch (Exception ex)
					{
						flag = false;
						message = ex.GetBaseException().Message;
					}
					log.LogInfo((object)("Command " + Str(item, "op") + " " + text + ": " + message));
					lock (Gate)
					{
						Results.Add(new Dictionary<string, object>
						{
							{ "id", text },
							{ "ok", flag },
							{ "message", message }
						});
					}
				}
			}
		}

		private static void Remember(string id)
		{
			Done.Add(id);
			DoneOrder.Enqueue(id);
			while (DoneOrder.Count > 500)
			{
				Done.Remove(DoneOrder.Dequeue());
			}
		}

		private static bool Do(Dictionary<string, object> cmd, out string message)
		{
			ZNet instance = ZNet.instance;
			string text = Str(cmd, "player").Trim();
			string text2 = Str(cmd, "op");
			switch (text2)
			{
			case "kick":
			case "ban":
			case "unban":
			case "tell":
				if (!PlayerId.IsMatch(text))
				{
					message = "Not a player id";
					return false;
				}
				break;
			}
			switch (text2)
			{
			case "say":
			case "tell":
			{
				string text4 = Clean(Str(cmd, "text"));
				if (text4 == "")
				{
					message = "No text";
					return false;
				}
				long peer = 0L;
				if (text2 == "tell")
				{
					ZNetPeer val2 = FindPeer(text);
					if (val2 == null)
					{
						message = text + " is not online";
						return true;
					}
					peer = val2.m_uid;
				}
				Show(peer, text4);
				message = "Sent";
				return true;
			}
			case "kick":
			{
				ZNetPeer val = FindPeer(text);
				if (val == null)
				{
					message = text + " is not online";
					return true;
				}
				MarkRemoved(text, "kick");
				instance.Kick(Plugin.UserIdOf(val));
				message = "Kicked " + val.m_playerName;
				return true;
			}
			case "ban":
			{
				MarkRemoved(text, "ban");
				instance.Ban(text);
				ZNetPeer val3 = FindPeer(text);
				if (val3 != null)
				{
					instance.Kick(Plugin.UserIdOf(val3));
				}
				message = "Banned " + text;
				return true;
			}
			case "unban":
			{
				string text3 = Norm(text);
				instance.Unban(text3);
				if (Regex.IsMatch(text3, "^7656119\\d{10}$"))
				{
					instance.Unban("Steam_" + text3);
				}
				message = "Unbanned " + text;
				return true;
			}
			default:
				message = "Unknown action";
				return false;
			}
		}

		private static string Clean(string text)
		{
			text = Regex.Replace(text.Replace("<", "").Replace(">", ""), "\\s+", " ").Trim();
			if (text.Length <= 200)
			{
				return text;
			}
			return text.Substring(0, 200);
		}

		private static void Show(long peer, string text)
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			Send(peer, text);
			Repeats.Add(new Showing
			{
				Peer = peer,
				Text = text,
				Until = realtimeSinceStartup + 8f - 4f,
				Next = realtimeSinceStartup + 2f
			});
		}

		private static void Send(long peer, string text)
		{
			if (peer != 0L)
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(peer, "ShowMessage", new object[2] { 2, text });
				return;
			}
			foreach (ZNetPeer peer2 in ZNet.instance.GetPeers())
			{
				if (peer2.IsReady())
				{
					ZRoutedRpc.instance.InvokeRoutedRPC(peer2.m_uid, "ShowMessage", new object[2] { 2, text });
				}
			}
		}

		private static void RepeatNotices(float now)
		{
			for (int num = Repeats.Count - 1; num >= 0; num--)
			{
				Showing showing = Repeats[num];
				if (now > showing.Until + 0.5f)
				{
					Repeats.RemoveAt(num);
				}
				else if (!(now < showing.Next))
				{
					showing.Next = now + 2f;
					Send(showing.Peer, showing.Text);
				}
			}
		}

		private static ZNetPeer FindPeer(string player)
		{
			string want = Norm(player);
			return ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.IsReady() && Norm(Plugin.UserIdOf(p)) == want));
		}

		private static string Str(Dictionary<string, object> d, string key)
		{
			if (!d.TryGetValue(key, out var value) || value == null)
			{
				return "";
			}
			return Convert.ToString(value, CultureInfo.InvariantCulture);
		}
	}
	internal static class Json
	{
		public static string Write(object value)
		{
			StringBuilder stringBuilder = new StringBuilder();
			Append(stringBuilder, value);
			return stringBuilder.ToString();
		}

		private static void Append(StringBuilder sb, object v)
		{
			if (v != null)
			{
				if (!(v is string s))
				{
					if (!(v is bool flag))
					{
						if (!(v is int) && !(v is long) && !(v is short) && !(v is byte) && !(v is uint) && !(v is ulong))
						{
							if (!(v is float) && !(v is double) && !(v is decimal))
							{
								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;
											Append(sb, item);
										}
										sb.Append(']');
									}
									else
									{
										AppendString(sb, v.ToString());
									}
									return;
								}
								sb.Append('{');
								bool flag3 = true;
								foreach (KeyValuePair<string, object> item2 in dictionary)
								{
									if (!flag3)
									{
										sb.Append(',');
									}
									flag3 = false;
									AppendString(sb, item2.Key);
									sb.Append(':');
									Append(sb, item2.Value);
								}
								sb.Append('}');
							}
							else
							{
								double d = Convert.ToDouble(v, CultureInfo.InvariantCulture);
								if (double.IsNaN(d) || double.IsInfinity(d))
								{
									sb.Append("null");
								}
								else
								{
									sb.Append(d.ToString("R", CultureInfo.InvariantCulture));
								}
							}
						}
						else
						{
							sb.Append(Convert.ToString(v, CultureInfo.InvariantCulture));
						}
					}
					else
					{
						sb.Append(flag ? "true" : "false");
					}
				}
				else
				{
					AppendString(sb, s);
				}
			}
			else
			{
				sb.Append("null");
			}
		}

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

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

		private static object ParseValue(string t, ref int i)
		{
			SkipSpace(t, ref i);
			if (i >= t.Length)
			{
				throw new FormatException("Unexpected end of JSON");
			}
			switch (t[i])
			{
			case '{':
			{
				Dictionary<string, object> dictionary = new Dictionary<string, object>();
				i++;
				SkipSpace(t, ref i);
				if (i < t.Length && t[i] == '}')
				{
					i++;
					return dictionary;
				}
				while (true)
				{
					SkipSpace(t, ref i);
					string key = ParseString(t, ref i);
					SkipSpace(t, ref i);
					Expect(t, ref i, ':');
					dictionary[key] = ParseValue(t, ref i);
					SkipSpace(t, ref i);
					if (i >= t.Length || t[i] != ',')
					{
						break;
					}
					i++;
				}
				Expect(t, ref i, '}');
				return dictionary;
			}
			case '[':
			{
				List<object> list = new List<object>();
				i++;
				SkipSpace(t, ref i);
				if (i < t.Length && t[i] == ']')
				{
					i++;
					return list;
				}
				while (true)
				{
					list.Add(ParseValue(t, ref i));
					SkipSpace(t, ref i);
					if (i >= t.Length || t[i] != ',')
					{
						break;
					}
					i++;
				}
				Expect(t, ref i, ']');
				return list;
			}
			case '"':
				return ParseString(t, ref i);
			default:
			{
				if (Word(t, ref i, "true"))
				{
					return true;
				}
				if (Word(t, ref i, "false"))
				{
					return false;
				}
				if (Word(t, ref i, "null"))
				{
					return null;
				}
				int num = i;
				while (i < t.Length && "+-0123456789.eE".IndexOf(t[i]) >= 0)
				{
					i++;
				}
				if (num == i)
				{
					throw new FormatException("Unexpected character in JSON");
				}
				return double.Parse(t.Substring(num, i - num), NumberStyles.Float, CultureInfo.InvariantCulture);
			}
			}
		}

		private static bool Word(string t, ref int i, string w)
		{
			if (string.CompareOrdinal(t, i, w, 0, w.Length) != 0)
			{
				return false;
			}
			i += w.Length;
			return true;
		}

		private static void Expect(string t, ref int i, char c)
		{
			if (i >= t.Length || t[i] != c)
			{
				throw new FormatException("Expected '" + c + "' in JSON");
			}
			i++;
		}

		private static string ParseString(string t, ref int i)
		{
			Expect(t, ref i, '"');
			StringBuilder stringBuilder = new StringBuilder();
			while (i < t.Length)
			{
				char c = t[i++];
				switch (c)
				{
				case '"':
					return stringBuilder.ToString();
				default:
					stringBuilder.Append(c);
					break;
				case '\\':
				{
					if (i >= t.Length)
					{
						throw new FormatException("Unfinished string in JSON");
					}
					char c2 = t[i++];
					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 (i + 4 > t.Length)
						{
							throw new FormatException("Bad escape in JSON");
						}
						stringBuilder.Append((char)int.Parse(t.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
						i += 4;
						break;
					default:
						stringBuilder.Append(c2);
						break;
					}
					break;
				}
				}
			}
			throw new FormatException("Unfinished string in JSON");
		}

		private static void AppendString(StringBuilder sb, string s)
		{
			sb.Append('"');
			foreach (char c in s)
			{
				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 < ' ')
				{
					StringBuilder stringBuilder = sb.Append("\\u");
					int num = c;
					stringBuilder.Append(num.ToString("x4"));
				}
				else
				{
					sb.Append(c);
				}
			}
			sb.Append('"');
		}
	}
	[BepInPlugin("com.findgameserver.mod", "FindGameServer Mod", "0.1.2")]
	public class Plugin : BaseUnityPlugin
	{
		private class Seen
		{
			public string Name;

			public string UserId;

			public float SpawnedAt = -1f;

			public bool ArrivalShoutSkipped;
		}

		private class Recent
		{
			public string Text;

			public float At;

			public Dictionary<string, object> Entry;
		}

		public const string Guid = "com.findgameserver.mod";

		public const string Name = "FindGameServer Mod";

		public const string Version = "0.1.2";

		private const float CheckPlayersEverySeconds = 2f;

		private const float ChatFlushSeconds = 3f;

		private const float FullDetailsEverySeconds = 600f;

		private const int MaxQueuedChat = 200;

		private const int MaxText = 500;

		private const float ChatDedupeSeconds = 3f;

		private static readonly Dictionary<string, string> BossNames = new Dictionary<string, string>
		{
			{ "defeated_eikthyr", "Eikthyr" },
			{ "defeated_gdking", "The Elder" },
			{ "defeated_bonemass", "Bonemass" },
			{ "defeated_dragon", "Moder" },
			{ "defeated_goblinking", "Yagluth" },
			{ "defeated_queen", "The Queen" },
			{ "defeated_fader", "Fader" }
		};

		internal static Plugin Instance;

		private ConfigEntry<string> key;

		private ConfigEntry<string> url;

		private ConfigEntry<int> reportEverySeconds;

		private ConfigEntry<bool> sendChat;

		private ConfigEntry<bool> sendNearbyChat;

		private ConfigEntry<bool> allowCommands;

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

		private readonly List<Dictionary<string, object>> chatQueue = new List<Dictionary<string, object>>();

		private float firstQueuedAt = -1f;

		private float lastReportAt = -1000f;

		private int sending;

		private volatile bool putBack;

		private volatile bool justFailed;

		private float lastFailAt = -1000f;

		private string lastBansSent;

		private float lastFullDetailsAt = -10000f;

		private float lastPlayerCheckAt;

		private int framesSinceReport;

		private float fpsWindowStart;

		private bool warnedNoKey;

		private int failuresInARow;

		private readonly Dictionary<long, Seen> seen = new Dictionary<long, Seen>();

		private readonly Dictionary<string, Recent> recentChat = new Dictionary<string, Recent>();

		private bool loggedFirstChat;

		private const float ArrivalShoutSeconds = 20f;

		private static readonly (string Name, (string Label, string[] Keys)[] Options)[] Sliders = new(string, (string, string[])[])[5]
		{
			("combat", new(string, string[])[4]
			{
				("Very easy", new string[3] { "playerdamage 125", "enemydamage 50", "enemyspeedsize 90" }),
				("Easy", new string[3] { "playerdamage 110", "enemydamage 75", "enemyspeedsize 95" }),
				("Hard", new string[4] { "playerdamage 85", "enemydamage 150", "enemyspeedsize 110", "enemyleveluprate 120" }),
				("Very hard", new string[4] { "playerdamage 70", "enemydamage 200", "enemyspeedsize 120", "enemyleveluprate 140" })
			}),
			("deathPenalty", new(string, string[])[5]
			{
				("Casual", new string[2] { "deathkeepequip", "skillreductionrate 15" }),
				("Very easy", new string[1] { "skillreductionrate 15" }),
				("Easy", new string[1] { "skillreductionrate 50" }),
				("Hard", new string[2] { "deathdeleteunequipped", "skillreductionrate 150" }),
				("Hardcore", new string[2] { "deathdeleteitems", "deathskillsreset" })
			}),
			("resources", new(string, string[])[5]
			{
				("0.5x", new string[1] { "resourcerate 50" }),
				("0.75x", new string[1] { "resourcerate 75" }),
				("1.5x", new string[1] { "resourcerate 150" }),
				("2x", new string[1] { "resourcerate 200" }),
				("3x", new string[1] { "resourcerate 300" })
			}),
			("raids", new(string, string[])[5]
			{
				("None", new string[1] { "eventrate 0" }),
				("Much less", new string[1] { "eventrate 200" }),
				("Less", new string[1] { "eventrate 150" }),
				("More", new string[1] { "eventrate 60" }),
				("Much more", new string[1] { "eventrate 30" })
			}),
			("portals", new(string, string[])[3]
			{
				("Items allowed", new string[1] { "teleportall" }),
				("No boss portals", new string[1] { "nobossportals" }),
				("No portals", new string[1] { "noportals" })
			})
		};

		private static readonly (string Key, string Label)[] Switches = new(string, string)[5]
		{
			("nobuildcost", "No build cost"),
			("passivemobs", "Passive enemies"),
			("nomap", "No map"),
			("playerevents", "Player-based raids"),
			("fire", "Fire hazards")
		};

		private void Awake()
		{
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			key = ((BaseUnityPlugin)this).Config.Bind<string>("FindGameServer", "Key", "", "Paste your server's key from Live stats on FindGameServer here, then restart the server.");
			url = ((BaseUnityPlugin)this).Config.Bind<string>("FindGameServer", "Url", "https://www.findgameserver.com/api/mod/report", "Where reports go. Leave as it is unless FindGameServer support asks you to change it.");
			reportEverySeconds = ((BaseUnityPlugin)this).Config.Bind<int>("FindGameServer", "ReportEverySeconds", 30, "How often the player list is sent, in seconds. Chat is sent within a few seconds.");
			sendChat = ((BaseUnityPlugin)this).Config.Bind<bool>("FindGameServer", "SendChat", true, "Send chat to FindGameServer, for your Live stats and your Discord. Whispers are never sent. Set to false to keep all chat on the server.");
			sendNearbyChat = ((BaseUnityPlugin)this).Config.Bind<bool>("FindGameServer", "SendNearbyChat", true, "Also send normal chat (what players say without /s, which in game only people nearby see). Set to false to send shouts only.");
			allowCommands = ((BaseUnityPlugin)this).Config.Bind<bool>("FindGameServer", "AllowCommands", false, "Let FindGameServer kick, ban and unban players and show notices in game, when you or your team use those buttons on FindGameServer or in Discord. Off unless you turn it on.");
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			}
			catch
			{
			}
			new Harmony("com.findgameserver.mod").PatchAll(typeof(Plugin).Assembly);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"FindGameServer Mod 0.1.2 loaded");
		}

		private static bool IsRunningServer()
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance != (Object)null && instance.IsServer() && instance.IsDedicated())
			{
				return ZNet.World != null;
			}
			return false;
		}

		private void Update()
		{
			if (!IsRunningServer())
			{
				return;
			}
			framesSinceReport++;
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - lastPlayerCheckAt >= 2f)
			{
				lastPlayerCheckAt = realtimeSinceStartup;
				CheckPlayers();
			}
			string text = key.Value.Trim();
			if (text.Length == 0)
			{
				if (!warnedNoKey)
				{
					warnedNoKey = true;
					((BaseUnityPlugin)this).Logger.LogWarning((object)"No key set, so nothing is sent. Paste your key from FindGameServer's Live stats into BepInEx/config/com.findgameserver.mod.cfg and restart.");
				}
				lock (chatQueue)
				{
					chatQueue.Clear();
					return;
				}
			}
			if (allowCommands.Value)
			{
				Commands.Tick(url.Value.Trim(), text, ((BaseUnityPlugin)this).Logger);
			}
			if (Volatile.Read(in sending) != 0)
			{
				return;
			}
			if (justFailed)
			{
				justFailed = false;
				lastFailAt = realtimeSinceStartup;
			}
			int num = Volatile.Read(in failuresInARow);
			if (num > 0 && realtimeSinceStartup - lastFailAt < Math.Min(60f, 3f * (float)(1 << Math.Min(num - 1, 5))))
			{
				return;
			}
			if (putBack)
			{
				putBack = false;
				if (firstQueuedAt < 0f)
				{
					firstQueuedAt = realtimeSinceStartup;
				}
			}
			if (realtimeSinceStartup - lastReportAt >= (float)Math.Max(10, reportEverySeconds.Value))
			{
				SendFullReport(realtimeSinceStartup);
			}
			else if (firstQueuedAt >= 0f && realtimeSinceStartup - firstQueuedAt >= 3f)
			{
				Dictionary<string, object> dictionary = NewReport();
				List<Dictionary<string, object>> chat = (List<Dictionary<string, object>>)(dictionary["chat"] = TakeChat());
				Post(dictionary, chat);
			}
		}

		private static IEnumerable<ZNetPeer> OnlinePeers()
		{
			return from p in ZNet.instance.GetPeers()
				where p != null && p.IsReady() && !string.IsNullOrEmpty(p.m_playerName)
				select p;
		}

		internal static string UserIdOf(ZNetPeer peer)
		{
			try
			{
				ISocket socket = peer.m_socket;
				return ((socket != null) ? socket.GetHostName() : null) ?? "";
			}
			catch
			{
				return "";
			}
		}

		private void CheckPlayers()
		{
			Dictionary<long, ZNetPeer> current = new Dictionary<long, ZNetPeer>();
			foreach (ZNetPeer item in OnlinePeers())
			{
				current[item.m_uid] = item;
			}
			foreach (ZNetPeer value2 in current.Values)
			{
				if (!this.seen.TryGetValue(value2.m_uid, out var value))
				{
					string userId = UserIdOf(value2);
					Dictionary<long, Seen> dictionary = this.seen;
					long uid = value2.m_uid;
					Seen obj = new Seen
					{
						Name = value2.m_playerName,
						UserId = userId
					};
					value = obj;
					dictionary[uid] = obj;
					QueueEvent("join", value2.m_playerName, userId, value2.m_playerName + " joined the server");
				}
				NoteSpawn(value, value2);
			}
			foreach (long item2 in this.seen.Keys.Where((long u) => !current.ContainsKey(u)).ToList())
			{
				Seen seen = this.seen[item2];
				this.seen.Remove(item2);
				string text = Commands.WhyRemoved(seen.UserId);
				if (!(text == "kick"))
				{
					if (text == "ban")
					{
						QueueEvent("ban", seen.Name, seen.UserId, seen.Name + " was banned");
					}
					else
					{
						QueueEvent("leave", seen.Name, seen.UserId, seen.Name + " left the server");
					}
				}
				else
				{
					QueueEvent("kick", seen.Name, seen.UserId, seen.Name + " was kicked");
				}
			}
		}

		private static void NoteSpawn(Seen s, ZNetPeer peer)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			if (s.SpawnedAt < 0f && peer.m_characterID != ZDOID.None)
			{
				s.SpawnedAt = Time.realtimeSinceStartup;
			}
		}

		private static List<object> PlayerList()
		{
			List<object> list = new List<object>();
			foreach (ZNetPeer item in OnlinePeers())
			{
				list.Add(new Dictionary<string, object>
				{
					{ "name", item.m_playerName },
					{
						"userId",
						UserIdOf(item)
					}
				});
			}
			return list;
		}

		private static long UnixNow()
		{
			return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
		}

		private void Enqueue(Dictionary<string, object> entry)
		{
			lock (chatQueue)
			{
				if (chatQueue.Count >= 200)
				{
					chatQueue.RemoveAt(0);
				}
				chatQueue.Add(entry);
			}
			if (firstQueuedAt < 0f)
			{
				firstQueuedAt = Time.realtimeSinceStartup;
			}
		}

		internal void OnDeath(long senderPeer, ZDOID target)
		{
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			if (IsRunningServer())
			{
				ZNetPeer peer = ZNet.instance.GetPeer(senderPeer);
				if (peer != null && !(peer.m_characterID != target) && seen.TryGetValue(senderPeer, out var value))
				{
					QueueEvent("death", value.Name, value.UserId, value.Name + " died");
				}
			}
		}

		private void QueueEvent(string ev, string name, string userId, string text)
		{
			Enqueue(new Dictionary<string, object>
			{
				{
					"at",
					UnixNow()
				},
				{ "system", true },
				{ "event", ev },
				{ "sender", name },
				{ "userId", userId },
				{ "text", text }
			});
		}

		internal bool OnChat(long senderPeer, long targetPeer, Type type, UserInfo user, string text)
		{
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0051: Invalid comparison between Unknown and I4
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Invalid comparison between Unknown and I4
			//IL_017d: Unknown result type (might be due to invalid IL or missing references)
			//IL_017f: Invalid comparison between Unknown and I4
			if (!IsRunningServer() || !sendChat.Value)
			{
				return false;
			}
			if (!loggedFirstChat)
			{
				loggedFirstChat = true;
				((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("Chat is reaching the server (first line: {0}, to {1})", type, (targetPeer == 0L) ? "everyone" : "one player"));
			}
			string text2;
			if ((int)type == 2)
			{
				text2 = "shout";
			}
			else
			{
				if ((int)type != 1 || !sendNearbyChat.Value)
				{
					return false;
				}
				text2 = "nearby";
			}
			if (string.IsNullOrWhiteSpace(text))
			{
				return false;
			}
			text = text.Trim();
			if (text.StartsWith("/"))
			{
				return false;
			}
			if (text.Length > 500)
			{
				text = text.Substring(0, 500);
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			string text3 = senderPeer + "|" + text2;
			if (recentChat.TryGetValue(text3, out var value) && realtimeSinceStartup - value.At <= 3f && SameLine(value.Text, text))
			{
				if (Stars(text) < Stars(value.Text))
				{
					value.Text = text;
					lock (chatQueue)
					{
						value.Entry["text"] = text;
					}
				}
				return false;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(senderPeer);
			seen.TryGetValue(senderPeer, out var value2);
			if (value2 != null && peer != null)
			{
				NoteSpawn(value2, peer);
			}
			if ((int)type == 2 && value2 != null && !value2.ArrivalShoutSkipped && (value2.SpawnedAt < 0f || realtimeSinceStartup - value2.SpawnedAt < 20f))
			{
				value2.ArrivalShoutSkipped = true;
				recentChat[text3] = new Recent
				{
					Text = text,
					At = realtimeSinceStartup,
					Entry = new Dictionary<string, object>()
				};
				return false;
			}
			Dictionary<string, object> entry = new Dictionary<string, object>
			{
				{
					"at",
					UnixNow()
				},
				{ "category", text2 },
				{
					"sender",
					peer?.m_playerName ?? user?.Name ?? ""
				},
				{
					"userId",
					(peer != null) ? UserIdOf(peer) : (((object)Unsafe.As<PlatformUserID, PlatformUserID>(ref user?.UserId)/*cast due to .constrained prefix*/).ToString() ?? "")
				},
				{ "text", text }
			};
			recentChat[text3] = new Recent
			{
				Text = text,
				At = realtimeSinceStartup,
				Entry = entry
			};
			Enqueue(entry);
			return true;
		}

		private static bool SameLine(string a, string b)
		{
			if (a == b)
			{
				return true;
			}
			if (a.Length != b.Length)
			{
				return false;
			}
			for (int i = 0; i < a.Length; i++)
			{
				if (a[i] != b[i] && a[i] != '*' && b[i] != '*')
				{
					return false;
				}
			}
			return true;
		}

		private static int Stars(string s)
		{
			return s.Count((char c) => c == '*');
		}

		private List<Dictionary<string, object>> TakeChat()
		{
			firstQueuedAt = -1f;
			lock (chatQueue)
			{
				List<Dictionary<string, object>> result = chatQueue.ToList();
				chatQueue.Clear();
				return result;
			}
		}

		private void PutBack(List<Dictionary<string, object>> chat)
		{
			lock (chatQueue)
			{
				chatQueue.InsertRange(0, chat);
				if (chatQueue.Count > 200)
				{
					chatQueue.RemoveRange(0, chatQueue.Count - 200);
				}
			}
			putBack = true;
		}

		private static string CommandLineValue(string flag)
		{
			string[] commandLineArgs = Environment.GetCommandLineArgs();
			for (int i = 0; i < commandLineArgs.Length - 1; i++)
			{
				if (string.Equals(commandLineArgs[i], flag, StringComparison.OrdinalIgnoreCase))
				{
					return commandLineArgs[i + 1];
				}
			}
			return null;
		}

		private static bool CommandLineHas(string flag)
		{
			return Environment.GetCommandLineArgs().Any((string a) => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
		}

		private Dictionary<string, object> ServerInfo(float now, int players)
		{
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			float num = Math.Max(0.001f, now - fpsWindowStart);
			float num2 = (float)framesSinceReport / num;
			framesSinceReport = 0;
			fpsWindowStart = now;
			return new Dictionary<string, object>
			{
				{
					"name",
					CommandLineValue("-name") ?? ""
				},
				{
					"world",
					ZNet.World?.m_name ?? ""
				},
				{
					"version",
					((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString()
				},
				{ "players", players },
				{
					"fps",
					Math.Round(num2, 1)
				},
				{
					"uptime",
					Math.Floor(now)
				},
				{
					"crossplay",
					CommandLineHas("-crossplay")
				}
			};
		}

		private static List<string> GlobalKeys()
		{
			try
			{
				ZoneSystem instance = ZoneSystem.instance;
				return ((instance != null) ? instance.GetGlobalKeys() : null) ?? new List<string>();
			}
			catch
			{
				return new List<string>();
			}
		}

		private static Dictionary<string, string> GlobalKeyValues(List<string> raw)
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>();
			foreach (string item in raw)
			{
				int num = item.IndexOf(' ');
				string text = ((num < 0) ? item : item.Substring(0, num)).ToLowerInvariant();
				dictionary[text] = ((num < 0) ? "" : item.Substring(num + 1).Trim());
			}
			return dictionary;
		}

		private static Dictionary<string, object> WorldInfo(List<string> keys)
		{
			long num = -1L;
			try
			{
				if ((Object)(object)EnvMan.instance != (Object)null)
				{
					num = EnvMan.instance.GetDay(ZNet.instance.GetTimeSeconds());
				}
			}
			catch
			{
			}
			List<object> list = new List<object>();
			foreach (KeyValuePair<string, string> boss in BossNames)
			{
				if (keys.Any((string k) => string.Equals(k, boss.Key, StringComparison.OrdinalIgnoreCase)))
				{
					list.Add(boss.Value);
				}
			}
			return new Dictionary<string, object>
			{
				{ "day", num },
				{ "bosses", list },
				{ "bossesTotal", BossNames.Count }
			};
		}

		private static Dictionary<string, object> WorldSettings(Dictionary<string, string> present)
		{
			Dictionary<string, object> dictionary = new Dictionary<string, object>();
			foreach (KeyValuePair<string, string> item in present)
			{
				if (!item.Key.StartsWith("defeated_", StringComparison.Ordinal))
				{
					dictionary[item.Key] = item.Value;
				}
			}
			return dictionary;
		}

		private static Dictionary<string, object> WorldModifiers(Dictionary<string, string> present)
		{
			Dictionary<string, object> dictionary = new Dictionary<string, object>();
			bool flag = false;
			(string, (string, string[])[])[] sliders = Sliders;
			for (int i = 0; i < sliders.Length; i++)
			{
				(string, (string, string[])[]) tuple = sliders[i];
				List<string> source = tuple.Item2.SelectMany(((string Label, string[] Keys) o) => o.Keys).Select(KeyName).Distinct()
					.ToList();
				HashSet<string> have = new HashSet<string>(from k in source.Where(present.ContainsKey)
					select Normalise((k + " " + present[k]).Trim()));
				string text;
				if (have.Count == 0)
				{
					text = "Normal";
				}
				else
				{
					text = tuple.Item2.FirstOrDefault(((string Label, string[] Keys) o) => have.SetEquals(o.Keys.Select(Normalise))).Label ?? "Custom";
					if (text == "Custom" && tuple.Item1 == "resources" && present.TryGetValue("resourcerate", out var value) && float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
					{
						text = (result / 100f).ToString("0.##", CultureInfo.InvariantCulture) + "x";
					}
					flag = true;
				}
				dictionary[tuple.Item1] = text;
			}
			List<object> list = (List<object>)(dictionary["switches"] = Switches.Where(((string Key, string Label) sw) => present.ContainsKey(sw.Key)).Select<(string, string), object>((Func<(string, string), object>)(((string Key, string Label) sw) => sw.Label)).ToList());
			if (list.Count > 0)
			{
				flag = true;
			}
			present.TryGetValue("preset", out var value2);
			if (!string.IsNullOrEmpty(value2) && !value2.Contains(":") && !value2.Contains("_"))
			{
				dictionary["preset"] = Title(value2);
			}
			else
			{
				dictionary["preset"] = (flag ? "Custom" : "Normal");
			}
			return dictionary;
		}

		private static string KeyName(string keyAndValue)
		{
			return keyAndValue.Split(new char[1] { ' ' })[0];
		}

		private static string Normalise(string keyAndValue)
		{
			string[] array = keyAndValue.Split(new char[1] { ' ' });
			if (array.Length < 2)
			{
				return array[0];
			}
			if (!float.TryParse(array[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
			{
				return keyAndValue;
			}
			return array[0] + " " + result.ToString(CultureInfo.InvariantCulture);
		}

		private static string Title(string s)
		{
			if (s.Length != 0)
			{
				return char.ToUpperInvariant(s[0]) + s.Substring(1).ToLowerInvariant();
			}
			return s;
		}

		private static List<object> ModList()
		{
			List<object> list = new List<object>();
			foreach (PluginInfo value in Chainloader.PluginInfos.Values)
			{
				BepInPlugin metadata = value.Metadata;
				list.Add(new Dictionary<string, object>
				{
					{ "guid", metadata.GUID },
					{ "name", metadata.Name },
					{
						"version",
						metadata.Version?.ToString() ?? ""
					}
				});
			}
			return list;
		}

		private Dictionary<string, object> NewReport()
		{
			return new Dictionary<string, object>
			{
				{ "v", 3 },
				{ "game", "valheim" },
				{ "modVersion", "0.1.2" },
				{
					"token",
					key.Value.Trim()
				},
				{ "commandsAllowed", allowCommands.Value }
			};
		}

		private void SendFullReport(float now)
		{
			lastReportAt = now;
			Dictionary<string, object> dictionary = NewReport();
			List<object> list = PlayerList();
			List<string> list2 = GlobalKeys();
			dictionary["server"] = ServerInfo(now, list.Count);
			dictionary["players"] = list;
			dictionary["numPlayers"] = list.Count;
			dictionary["world"] = WorldInfo(list2);
			if (now - lastFullDetailsAt >= 600f)
			{
				lastFullDetailsAt = now;
				Dictionary<string, string> present = GlobalKeyValues(list2);
				dictionary["settings"] = WorldSettings(present);
				dictionary["modifiers"] = WorldModifiers(present);
				dictionary["mods"] = ModList();
			}
			List<string> banned = ZNet.instance.Banned;
			string text = string.Join("\n", banned);
			if (text != lastBansSent)
			{
				lastBansSent = text;
				dictionary["bans"] = banned.ToList();
			}
			List<Dictionary<string, object>> chat = (List<Dictionary<string, object>>)(dictionary["chat"] = TakeChat());
			Post(dictionary, chat);
		}

		private void Post(Dictionary<string, object> body, List<Dictionary<string, object>> chat)
		{
			string target = (url.Value ?? "").Trim();
			if (!target.StartsWith("https://") && !target.StartsWith("http://"))
			{
				return;
			}
			Interlocked.Exchange(ref sending, 1);
			string json = Json.Write(body);
			ManualLogSource log = ((BaseUnityPlugin)this).Logger;
			Task.Run(async delegate
			{
				bool retry = true;
				try
				{
					HttpResponseMessage res = await PostOnce(target, json).ConfigureAwait(continueOnCapturedContext: false);
					try
					{
						string text = await res.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);
						if (res.IsSuccessStatusCode)
						{
							retry = false;
							if (Interlocked.Exchange(ref failuresInARow, 0) > 0)
							{
								log.LogInfo((object)"Reports are getting through again");
							}
							try
							{
								Commands.OnReportReply(Json.Parse(text));
								return;
							}
							catch
							{
								return;
							}
						}
						int statusCode = (int)res.StatusCode;
						retry = statusCode == 429 || statusCode >= 500;
						if (Interlocked.Increment(ref failuresInARow) % 20 == 1)
						{
							log.LogWarning((object)$"FindGameServer refused the report ({statusCode}): {Trim(text)}");
						}
					}
					finally
					{
						((IDisposable)res)?.Dispose();
					}
				}
				catch (Exception ex)
				{
					if (Interlocked.Increment(ref failuresInARow) % 20 == 1)
					{
						log.LogWarning((object)("Couldn't reach FindGameServer: " + ex.GetBaseException().Message));
					}
				}
				finally
				{
					if (retry && chat.Count > 0)
					{
						PutBack(chat);
					}
					if (Volatile.Read(in failuresInARow) > 0)
					{
						justFailed = true;
					}
					Interlocked.Exchange(ref sending, 0);
				}
			});
		}

		private static async Task<HttpResponseMessage> PostOnce(string target, string json)
		{
			int attempt = 0;
			while (true)
			{
				try
				{
					StringContent content = new StringContent(json, Encoding.UTF8, "application/json");
					try
					{
						return await Http.PostAsync(target, (HttpContent)(object)content).ConfigureAwait(continueOnCapturedContext: false);
					}
					finally
					{
						((IDisposable)content)?.Dispose();
					}
				}
				catch (object obj) when (obj is HttpRequestException && attempt == 0)
				{
				}
				attempt++;
			}
		}

		private static string Trim(string s)
		{
			if (s != null)
			{
				if (s.Length <= 200)
				{
					return s;
				}
				return s.Substring(0, 200);
			}
			return "";
		}
	}
	[HarmonyPatch(typeof(ZRoutedRpc), "RPC_RoutedRPC")]
	internal static class ChatHook
	{
		private static readonly int ChatMessageHash = StringExtensionMethods.GetStableHashCode("ChatMessage");

		private static readonly int SayHash = StringExtensionMethods.GetStableHashCode("Say");

		private static readonly int OnDeathHash = StringExtensionMethods.GetStableHashCode("OnDeath");

		private static bool warned;

		private static void Prefix(ZPackage pkg)
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Expected O, but got Unknown
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Expected O, but got Unknown
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bf: Expected O, but got Unknown
			//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)Plugin.Instance == (Object)null || pkg == null)
			{
				return;
			}
			try
			{
				int pos = pkg.GetPos();
				int num;
				try
				{
					pkg.ReadLong();
					pkg.ReadLong();
					pkg.ReadLong();
					pkg.ReadZDOID();
					num = pkg.ReadInt();
				}
				finally
				{
					pkg.SetPos(pos);
				}
				if (num != ChatMessageHash && num != SayHash && num != OnDeathHash)
				{
					return;
				}
				RoutedRPCData val = new RoutedRPCData();
				val.Deserialize(new ZPackage(pkg.GetArray()));
				if (num == OnDeathHash)
				{
					Plugin.Instance.OnDeath(val.m_senderPeerID, val.m_targetZDO);
					return;
				}
				ZPackage parameters = val.m_parameters;
				if (num == ChatMessageHash)
				{
					parameters.ReadVector3();
				}
				Type type = (Type)parameters.ReadInt();
				UserInfo val2 = new UserInfo();
				val2.Deserialize(ref parameters);
				string text = parameters.ReadString();
				Plugin.Instance.OnChat(val.m_senderPeerID, val.m_targetPeerID, type, val2, text);
			}
			catch (Exception ex)
			{
				if (!warned)
				{
					warned = true;
					Debug.LogWarning((object)("[FindGameServer] chat hook: " + ex.Message));
				}
			}
		}
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}