Decompiled source of Skald v0.4.0

Skald.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Skald.Client;
using Skald.Net;
using Skald.Server;
using Skald.Util;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[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("Skald")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.4.0.0")]
[assembly: AssemblyInformationalVersion("0.4.0")]
[assembly: AssemblyProduct("Skald")]
[assembly: AssemblyTitle("Skald")]
[assembly: AssemblyVersion("0.4.0.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 Skald
{
	internal static class SkaldConfig
	{
		public static ConfigEntry<bool> Enabled;

		public static ConfigEntry<bool> AchievementsEnabled;

		public static ConfigEntry<bool> AnnounceInChat;

		public static ConfigEntry<string> AnnouncerName;

		public static ConfigEntry<bool> CountLifetimeStats;

		public static ConfigEntry<float> ReportIntervalSeconds;

		public static ConfigEntry<float> StartupGraceSeconds;

		public static ConfigEntry<bool> FirstToReach;

		public static ConfigEntry<bool> DeathAnnouncements;

		public static ConfigEntry<bool> DeathsToDiscord;

		public static ConfigEntry<bool> NemesisEnabled;

		public static ConfigEntry<int> NemesisThreshold;

		public static ConfigEntry<bool> StreaksEnabled;

		public static ConfigEntry<int> StreakEndMentionDays;

		public static ConfigEntry<bool> ChatCommands;

		public static ConfigEntry<bool> WhileYouWereAway;

		public static ConfigEntry<KeyboardShortcut> PanelKey;

		public static ConfigEntry<bool> PanelIcon;

		public static ConfigEntry<bool> PanelButtonExtendPanel;

		public static ConfigEntry<float> PanelButtonOffsetX;

		public static ConfigEntry<float> PanelButtonOffsetY;

		public static ConfigEntry<float> CatchupMinHours;

		public static ConfigEntry<bool> ReturnMessages;

		public static ConfigEntry<float> ReturnAfterDays;

		public static ConfigEntry<bool> BossSummaries;

		public static ConfigEntry<bool> WorldCensus;

		public static ConfigEntry<float> CensusIntervalHours;

		public static ConfigEntry<bool> RaidAnnouncements;

		public static ConfigEntry<bool> WatchdogEnabled;

		public static ConfigEntry<float> SampleIntervalSeconds;

		public static ConfigEntry<float> FrameWarnMs;

		public static ConfigEntry<float> FrameCriticalMs;

		public static ConfigEntry<int> SustainedSamples;

		public static ConfigEntry<int> MemoryWarnMb;

		public static ConfigEntry<int> MemoryCriticalMb;

		public static ConfigEntry<int> ObjectAlertCount;

		public static ConfigEntry<float> SaveWarnSeconds;

		public static ConfigEntry<float> AlertCooldownMinutes;

		public static ConfigEntry<string> DiscordWebhookUrl;

		public static ConfigEntry<bool> DiscordAnnounceAchievements;

		public static ConfigEntry<bool> RaidsToDiscord;

		public static ConfigEntry<bool> WeeklyRecap;

		public static ConfigEntry<DayOfWeek> RecapDay;

		public static ConfigEntry<int> RecapHour;

		public static ConfigEntry<bool> LiveStatus;

		public static ConfigEntry<int> LiveStatusIntervalSeconds;

		public static ConfigEntry<string> StatusWebhookUrl;

		public static ConfigEntry<string> StatusServerName;

		public static void Bind(ConfigFile cfg)
		{
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Expected O, but got Unknown
			//IL_016c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0176: Expected O, but got Unknown
			//IL_01e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b7: Expected O, but got Unknown
			//IL_0300: Unknown result type (might be due to invalid IL or missing references)
			//IL_030a: Expected O, but got Unknown
			//IL_036e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0378: Expected O, but got Unknown
			//IL_03dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_03e6: Expected O, but got Unknown
			//IL_0447: Unknown result type (might be due to invalid IL or missing references)
			//IL_0451: Expected O, but got Unknown
			//IL_04f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0501: Expected O, but got Unknown
			//IL_05cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_05d9: Expected O, but got Unknown
			//IL_063b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0645: Expected O, but got Unknown
			Enabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch. False leaves the game completely unpatched.");
			AchievementsEnabled = cfg.Bind<bool>("Achievements", "Enabled", true, "Track and announce achievements. Server-side setting.");
			AnnounceInChat = cfg.Bind<bool>("Achievements", "AnnounceInChat", true, "Show Skald's public announcements in game (achievements, deaths, raids, streaks). Server-side setting. Players need the mod to see them.");
			AnnouncerName = cfg.Bind<string>("Achievements", "AnnouncerName", "Skald", "Name shown in front of Skald's chat lines. Server-side setting.");
			CountLifetimeStats = cfg.Bind<bool>("Achievements", "CountLifetimeStats", false, "False (default): a player's stats are baselined when Skald first sees them, so achievements measure progress on THIS server only. True: count their whole character lifetime, which will instantly unlock a lot for anyone with an established character.");
			ReportIntervalSeconds = cfg.Bind<float>("Achievements", "ReportIntervalSeconds", 30f, new ConfigDescription("How often a client reports its stats to the server.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 300f), Array.Empty<object>()));
			StartupGraceSeconds = cfg.Bind<float>("Achievements", "StartupGraceSeconds", 30f, "Announcements are suppressed for this long after the server starts, so loading an existing world does not spam every already-earned milestone.");
			FirstToReach = cfg.Bind<bool>("Achievements", "FirstToReachBiomes", true, "Call out the first player on the server to reach each biome.");
			DeathAnnouncements = cfg.Bind<bool>("Deaths", "Enabled", true, "Announce deaths, with the cause and whatever did it. Read by both halves: a client with this off reports nothing.");
			DeathsToDiscord = cfg.Bind<bool>("Deaths", "AlsoPostToDiscord", false, "Also send death announcements to the webhook. Server-side setting.");
			NemesisEnabled = cfg.Bind<bool>("Deaths", "Nemesis", true, "When the same kind of creature (or the same player) keeps killing someone, it becomes their nemesis. Killing one afterwards settles it.");
			NemesisThreshold = cfg.Bind<int>("Deaths", "NemesisThreshold", 3, new ConfigDescription("Deaths to the same killer before it becomes a nemesis.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(2, 20), Array.Empty<object>()));
			StreaksEnabled = cfg.Bind<bool>("Deaths", "SurvivalStreaks", true, "Announce in-game days survived without dying, at 5, 10, 20, 30, 50, 75 and 100.");
			StreakEndMentionDays = cfg.Bind<int>("Deaths", "StreakEndMentionDays", 3, "When someone dies with a streak at least this long, the death message says so.");
			ChatCommands = cfg.Bind<bool>("Social", "ChatCommands", true, "Enable !skald, !deaths, !top and !recap. Read by both halves.");
			PanelKey = cfg.Bind<KeyboardShortcut>("Social", "PanelKey", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Key that opens Skald's panel from anywhere. Client-side setting.");
			PanelIcon = cfg.Bind<bool>("Social", "PanelIcon", false, "Experimental: add a Skald rune to the row of icons on the inventory screen. Off by default - that row is prefab data and mods rearrange it, so the rune tends to land badly. Use the key above instead. Client-side setting.");
			PanelButtonExtendPanel = cfg.Bind<bool>("Social", "PanelButtonExtendPanel", true, "Widen the panel behind the row of inventory icons to make room, and put Skald first. False puts it after the last icon instead, which may overhang the panel. Client-side setting.");
			PanelButtonOffsetX = cfg.Bind<float>("Social", "PanelButtonOffsetX", 0f, "Nudge the Skald button sideways, in pixels, if another mod's layout leaves it somewhere awkward. Client-side setting.");
			PanelButtonOffsetY = cfg.Bind<float>("Social", "PanelButtonOffsetY", 0f, "Nudge the Skald button up or down, in pixels. Client-side setting.");
			WhileYouWereAway = cfg.Bind<bool>("Social", "WhileYouWereAway", true, "Privately tell an arriving player what they missed: bosses, what others earned, how many died, and whether their streak survived.");
			CatchupMinHours = cfg.Bind<float>("Social", "WhileYouWereAwayMinHours", 0.5f, new ConfigDescription("Hours away before that summary is worth sending.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 720f), Array.Empty<object>()));
			ReturnMessages = cfg.Bind<bool>("Social", "ReturnMessages", true, "Announce players coming back after a long absence.");
			ReturnAfterDays = cfg.Bind<float>("Social", "ReturnAfterDays", 3f, new ConfigDescription("Real days away that count as a long absence.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 365f), Array.Empty<object>()));
			BossSummaries = cfg.Bind<bool>("Events", "BossSummaries", true, "When a boss dies, report how long the fight took and who died in it.");
			WorldCensus = cfg.Bind<bool>("Events", "WorldCensus", true, "Periodically count what the world is made of, so growth can be blamed on something specific. The count is spread over many frames.");
			CensusIntervalHours = cfg.Bind<float>("Events", "CensusIntervalHours", 6f, new ConfigDescription("Hours between counts.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 168f), Array.Empty<object>()));
			RaidAnnouncements = cfg.Bind<bool>("Events", "RaidAnnouncements", true, "Announce raids to everyone, naming whose base is under attack, and report the casualties when they end.");
			WatchdogEnabled = cfg.Bind<bool>("Watchdog", "Enabled", true, "Monitor server performance. Server-side setting.");
			SampleIntervalSeconds = cfg.Bind<float>("Watchdog", "SampleIntervalSeconds", 10f, new ConfigDescription("Seconds per metrics sample.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 120f), Array.Empty<object>()));
			FrameWarnMs = cfg.Bind<float>("Watchdog", "FrameWarnMs", 50f, "Average frame time over a sample above this is a warning. The dedicated server normally sits far below this.");
			FrameCriticalMs = cfg.Bind<float>("Watchdog", "FrameCriticalMs", 120f, "Average frame time over a sample above this is critical.");
			SustainedSamples = cfg.Bind<int>("Watchdog", "SustainedSamples", 3, new ConfigDescription("Consecutive bad samples before alerting. Stops a single hitch from paging you.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20), Array.Empty<object>()));
			MemoryWarnMb = cfg.Bind<int>("Watchdog", "MemoryWarnMb", 2800, "Process memory warning threshold. Tuned for a 4 GB box.");
			MemoryCriticalMb = cfg.Bind<int>("Watchdog", "MemoryCriticalMb", 3400, "Process memory critical threshold. Tuned for a 4 GB box.");
			ObjectAlertCount = cfg.Bind<int>("Watchdog", "ObjectAlertCount", 0, "Optional: alert once the world holds more than this many networked objects. 0 (default) never alerts. The count only ever goes up - every area explored adds its trees, rocks and bushes for good - so Skald tracks it and reports its growth rather than warning about it.");
			SaveWarnSeconds = cfg.Bind<float>("Watchdog", "SaveWarnSeconds", 5f, "Warn when a world save blocks for longer than this.");
			AlertCooldownMinutes = cfg.Bind<float>("Watchdog", "AlertCooldownMinutes", 15f, new ConfigDescription("Minimum gap between repeats of the same alert.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1440f), Array.Empty<object>()));
			DiscordWebhookUrl = cfg.Bind<string>("Discord", "WebhookUrl", "", "Discord webhook URL. Empty (default) means nothing is ever sent anywhere. Paste your own webhook here to switch Discord on.");
			DiscordAnnounceAchievements = cfg.Bind<bool>("Discord", "AnnounceAchievements", false, "Also post achievement unlocks and boss summaries to the webhook.");
			RaidsToDiscord = cfg.Bind<bool>("Discord", "AnnounceRaids", false, "Also post raid start and end to the webhook.");
			WeeklyRecap = cfg.Bind<bool>("Discord", "WeeklyRecap", true, "Post a weekly summary to the webhook: hours, deaths, achievements, bosses, and server health with a capacity forecast.");
			RecapDay = cfg.Bind<DayOfWeek>("Discord", "RecapDay", DayOfWeek.Sunday, "Day of the week the recap posts, in the server's local time.");
			LiveStatus = cfg.Bind<bool>("Discord", "LiveStatus", true, "Keep one Discord message up to date with who is online and how the server is doing, edited in place rather than reposted. Pin it once. Needs a webhook.");
			LiveStatusIntervalSeconds = cfg.Bind<int>("Discord", "LiveStatusIntervalSeconds", 120, new ConfigDescription("Seconds between status updates.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(30, 3600), Array.Empty<object>()));
			StatusWebhookUrl = cfg.Bind<string>("Discord", "StatusWebhookUrl", "", "Optional separate webhook for the status message, e.g. for a #server-status channel. Empty uses WebhookUrl.");
			StatusServerName = cfg.Bind<string>("Discord", "StatusServerName", "", "Name shown on the status message. Empty uses the server's own name.");
			RecapHour = cfg.Bind<int>("Discord", "RecapHour", 18, new ConfigDescription("Hour (0-23) the recap posts, in the server's local time. If the server is down then, it posts when it next starts.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 23), Array.Empty<object>()));
		}
	}
	[BepInPlugin("com.dks.skald", "Skald", "0.4.0")]
	[BepInProcess("valheim.exe")]
	[BepInProcess("valheim_server.exe")]
	public class SkaldPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "com.dks.skald";

		public const string PluginName = "Skald";

		public const string PluginVersion = "0.4.0";

		internal static ManualLogSource Log;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			SkaldConfig.Bind(((BaseUnityPlugin)this).Config);
			if (!SkaldConfig.Enabled.Value)
			{
				Log.LogInfo((object)"Skald is disabled in config; not patching.");
				return;
			}
			_harmony = new Harmony("com.dks.skald");
			_harmony.PatchAll(Assembly.GetExecutingAssembly());
			Log.LogInfo((object)"Skald 0.4.0 loaded.");
		}

		private void Update()
		{
			SkaldRuntime.Tick(Time.deltaTime);
		}

		private void OnDestroy()
		{
			SkaldRuntime.Teardown();
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	internal static class SkaldRuntime
	{
		private static bool _started;

		public static bool IsServer { get; private set; }

		public static bool IsDedicated { get; private set; }

		public static bool HasLocalPlayer { get; private set; }

		public static void Tick(float dt)
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				if (_started)
				{
					Teardown();
				}
				return;
			}
			if (!_started)
			{
				Startup(instance);
			}
			Protocol.Register(IsServer, HasLocalPlayer);
			if (IsServer)
			{
				ServerHub.Tick(dt);
				Watchdog.Tick(dt);
			}
			if (HasLocalPlayer)
			{
				ClientReporter.Tick(dt);
				PanelClient.Tick(dt);
			}
		}

		private static void Startup(ZNet net)
		{
			_started = true;
			IsServer = net.IsServer();
			IsDedicated = net.IsDedicated();
			HasLocalPlayer = !IsDedicated;
			if (IsServer)
			{
				ServerHub.Start();
				Watchdog.Start();
			}
			if (HasLocalPlayer)
			{
				ClientReporter.Start();
			}
			SkaldPlugin.Log.LogInfo((object)("Skald active (server=" + IsServer + " dedicated=" + IsDedicated + " localPlayer=" + HasLocalPlayer + ")."));
		}

		public static void Teardown()
		{
			if (_started)
			{
				_started = false;
				if (IsServer)
				{
					ServerHub.Stop();
					Watchdog.Stop();
				}
				if (HasLocalPlayer)
				{
					ClientReporter.Stop();
					PanelClient.Reset();
				}
				Protocol.Reset();
				IsServer = false;
				IsDedicated = false;
				HasLocalPlayer = false;
			}
		}
	}
}
namespace Skald.Util
{
	internal sealed class JsonValue
	{
		public enum Kind
		{
			Null,
			Bool,
			Number,
			String,
			Array,
			Object
		}

		public Kind Type;

		public bool Bool;

		public double Number;

		public string String;

		public List<JsonValue> Array;

		public Dictionary<string, JsonValue> Object;

		public static JsonValue NewObject()
		{
			return new JsonValue
			{
				Type = Kind.Object,
				Object = new Dictionary<string, JsonValue>()
			};
		}

		public static JsonValue NewArray()
		{
			return new JsonValue
			{
				Type = Kind.Array,
				Array = new List<JsonValue>()
			};
		}

		public static JsonValue Of(string s)
		{
			return new JsonValue
			{
				Type = Kind.String,
				String = s
			};
		}

		public static JsonValue Of(double d)
		{
			return new JsonValue
			{
				Type = Kind.Number,
				Number = d
			};
		}

		public static JsonValue Of(bool b)
		{
			return new JsonValue
			{
				Type = Kind.Bool,
				Bool = b
			};
		}

		public bool Has(string key)
		{
			if (Type == Kind.Object)
			{
				return Object.ContainsKey(key);
			}
			return false;
		}

		public JsonValue Get(string key)
		{
			if (Type != Kind.Object || !Object.TryGetValue(key, out var value))
			{
				return null;
			}
			return value;
		}

		public string GetString(string key, string fallback = null)
		{
			JsonValue jsonValue = Get(key);
			if (jsonValue == null || jsonValue.Type != Kind.String)
			{
				return fallback;
			}
			return jsonValue.String;
		}

		public double GetNumber(string key, double fallback = 0.0)
		{
			JsonValue jsonValue = Get(key);
			if (jsonValue == null || jsonValue.Type != Kind.Number)
			{
				return fallback;
			}
			return jsonValue.Number;
		}

		public bool GetBool(string key, bool fallback = false)
		{
			JsonValue jsonValue = Get(key);
			if (jsonValue == null || jsonValue.Type != Kind.Bool)
			{
				return fallback;
			}
			return jsonValue.Bool;
		}

		public void Set(string key, JsonValue value)
		{
			Object[key] = value;
		}

		public void Add(JsonValue value)
		{
			Array.Add(value);
		}
	}
	internal sealed class JsonException : Exception
	{
		public JsonException(string message)
			: base(message)
		{
		}
	}
	internal static class Json
	{
		public static JsonValue Parse(string text)
		{
			int i = 0;
			JsonValue result = ParseValue(text, ref i);
			SkipWhitespace(text, ref i);
			if (i < text.Length)
			{
				throw new JsonException("Unexpected text after the end at position " + i + ".");
			}
			return result;
		}

		public static string Write(JsonValue value, int indent = 0)
		{
			StringBuilder stringBuilder = new StringBuilder();
			WriteValue(stringBuilder, value, indent);
			return stringBuilder.ToString();
		}

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

		private static JsonValue ParseObject(string s, ref int i)
		{
			JsonValue jsonValue = JsonValue.NewObject();
			i++;
			SkipWhitespace(s, ref i);
			if (i < s.Length && s[i] == '}')
			{
				i++;
				return jsonValue;
			}
			while (true)
			{
				SkipWhitespace(s, ref i);
				if (i >= s.Length || s[i] != '"')
				{
					throw new JsonException("Expected a property name at position " + i + ".");
				}
				string text = ParseString(s, ref i);
				SkipWhitespace(s, ref i);
				if (i >= s.Length || s[i] != ':')
				{
					throw new JsonException("Expected ':' after '" + text + "'.");
				}
				i++;
				jsonValue.Object[text] = ParseValue(s, ref i);
				SkipWhitespace(s, ref i);
				if (i >= s.Length)
				{
					throw new JsonException("Unexpected end of file inside an object.");
				}
				if (s[i] != ',')
				{
					break;
				}
				i++;
			}
			if (s[i] == '}')
			{
				i++;
				return jsonValue;
			}
			throw new JsonException("Expected ',' or '}' at position " + i + ".");
		}

		private static JsonValue ParseArray(string s, ref int i)
		{
			JsonValue jsonValue = JsonValue.NewArray();
			i++;
			SkipWhitespace(s, ref i);
			if (i < s.Length && s[i] == ']')
			{
				i++;
				return jsonValue;
			}
			while (true)
			{
				jsonValue.Array.Add(ParseValue(s, ref i));
				SkipWhitespace(s, ref i);
				if (i >= s.Length)
				{
					throw new JsonException("Unexpected end of file inside an array.");
				}
				if (s[i] != ',')
				{
					break;
				}
				i++;
			}
			if (s[i] == ']')
			{
				i++;
				return jsonValue;
			}
			throw new JsonException("Expected ',' or ']' at position " + i + ".");
		}

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

		private static double ParseNumber(string s, ref int i)
		{
			int num = i;
			if (i < s.Length && (s[i] == '-' || s[i] == '+'))
			{
				i++;
			}
			while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '.' || s[i] == 'e' || s[i] == 'E' || ((s[i] == '-' || s[i] == '+') && (s[i - 1] == 'e' || s[i - 1] == 'E'))))
			{
				i++;
			}
			string text = s.Substring(num, i - num);
			if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
			{
				throw new JsonException("'" + text + "' is not a number (position " + num + ").");
			}
			return result;
		}

		private static void Expect(string s, ref int i, string word)
		{
			if (i + word.Length > s.Length || s.Substring(i, word.Length) != word)
			{
				throw new JsonException("Expected '" + word + "' at position " + i + ".");
			}
			i += word.Length;
		}

		private static void SkipWhitespace(string s, ref int i)
		{
			while (i < s.Length)
			{
				char c = s[i];
				if (c == '/' && i + 1 < s.Length && s[i + 1] == '/')
				{
					while (i < s.Length && s[i] != '\n')
					{
						i++;
					}
					continue;
				}
				if (!char.IsWhiteSpace(c))
				{
					break;
				}
				i++;
			}
		}

		private static void WriteValue(StringBuilder sb, JsonValue v, int indent)
		{
			if (v == null)
			{
				sb.Append("null");
				return;
			}
			switch (v.Type)
			{
			case JsonValue.Kind.Null:
				sb.Append("null");
				break;
			case JsonValue.Kind.Bool:
				sb.Append(v.Bool ? "true" : "false");
				break;
			case JsonValue.Kind.Number:
				sb.Append(v.Number.ToString("0.############", CultureInfo.InvariantCulture));
				break;
			case JsonValue.Kind.String:
				WriteString(sb, v.String);
				break;
			case JsonValue.Kind.Array:
				WriteArray(sb, v, indent);
				break;
			case JsonValue.Kind.Object:
				WriteObject(sb, v, indent);
				break;
			}
		}

		private static void WriteArray(StringBuilder sb, JsonValue v, int indent)
		{
			if (v.Array.Count == 0)
			{
				sb.Append("[]");
				return;
			}
			sb.Append("[\n");
			for (int i = 0; i < v.Array.Count; i++)
			{
				Indent(sb, indent + 1);
				WriteValue(sb, v.Array[i], indent + 1);
				if (i < v.Array.Count - 1)
				{
					sb.Append(',');
				}
				sb.Append('\n');
			}
			Indent(sb, indent);
			sb.Append(']');
		}

		private static void WriteObject(StringBuilder sb, JsonValue v, int indent)
		{
			if (v.Object.Count == 0)
			{
				sb.Append("{}");
				return;
			}
			sb.Append("{ ");
			int num = 0;
			foreach (KeyValuePair<string, JsonValue> item in v.Object)
			{
				WriteString(sb, item.Key);
				sb.Append(": ");
				WriteValue(sb, item.Value, indent + 1);
				if (num < v.Object.Count - 1)
				{
					sb.Append(", ");
				}
				num++;
			}
			sb.Append(" }");
		}

		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 < ' ')
				{
					StringBuilder stringBuilder = sb.Append("\\u");
					int num = c;
					stringBuilder.Append(num.ToString("x4"));
				}
				else
				{
					sb.Append(c);
				}
			}
			sb.Append('"');
		}

		private static void Indent(StringBuilder sb, int depth)
		{
			sb.Append(' ', depth * 2);
		}
	}
}
namespace Skald.Server
{
	internal enum AchievementScope
	{
		Player,
		World
	}
	internal sealed class AchievementDef
	{
		public string Id;

		public string Title;

		public string Description;

		public string Type;

		public string Stat;

		public double Target;

		public string Unit = "";

		public double Scale = 1.0;

		public double Hours;

		public string Biome;

		public string Key;

		public bool Hidden;
	}
	internal sealed class Achievement
	{
		public string Id;

		public string Title;

		public string Description;

		public AchievementScope Scope;

		public bool Hidden;

		public Func<PlayerRecord, bool> PlayerTest;

		public Func<WorldRecord, bool> WorldTest;

		public Func<PlayerRecord, double> Current;

		public double Target;

		public double DisplayScale = 1.0;

		public string Unit = "";

		public int BiomeId;

		public string BiomeName;
	}
	internal static class AchievementCatalog
	{
		private static List<Achievement> _all;

		public static List<Achievement> All
		{
			get
			{
				if (_all == null)
				{
					_all = CatalogFile.Compile(Defaults(), new List<string>());
				}
				return _all;
			}
		}

		public static int PlayerCount
		{
			get
			{
				int num = 0;
				foreach (Achievement item in All)
				{
					if (item.Scope == AchievementScope.Player)
					{
						num++;
					}
				}
				return num;
			}
		}

		public static int WorldCount => All.Count - PlayerCount;

		public static void Use(List<Achievement> achievements)
		{
			_all = achievements;
		}

		public static List<AchievementDef> Defaults()
		{
			return new List<AchievementDef>
			{
				Key("boss_eikthyr", "Antler Broken", "Eikthyr has fallen.", "defeated_eikthyr"),
				Key("boss_elder", "Root and Branch", "The Elder has fallen.", "defeated_gdking"),
				Key("boss_bonemass", "Rot Cleared", "Bonemass has fallen.", "defeated_bonemass"),
				Key("boss_moder", "Sky Torn", "Moder has fallen.", "defeated_dragon"),
				Key("boss_yagluth", "Ash of Kings", "Yagluth has fallen.", "defeated_goblinking"),
				Key("boss_queen", "Hive Silenced", "The Queen has fallen.", "defeated_queen"),
				Key("boss_fader", "Cinders", "Fader has fallen.", "defeated_fader"),
				Stat("kills_100", "Blooded", "100 creatures slain.", "EnemyKills", 100.0, "kills"),
				Stat("kills_1000", "Scourge of the Tenth World", "1000 creatures slain.", "EnemyKills", 1000.0, "kills"),
				Stat("arrows_1000", "Fletcher", "1000 arrows loosed.", "ArrowsShot", 1000.0, "arrows"),
				Stat("deaths_10", "Familiar with the Ferryman", "Died 10 times.", "Deaths", 10.0, "deaths"),
				Stat("deaths_50", "Stubborn", "Died 50 times and kept going.", "Deaths", 50.0, "deaths"),
				Stat("fall_10", "Gravity Wins", "Fell to your death 10 times.", "DeathByFall", 10.0, "falls"),
				Stat("drown_5", "Not a Swimmer", "Drowned 5 times.", "DeathByDrowning", 5.0, "drownings"),
				Stat("trees_500", "Woodsman", "500 trees felled.", "TreeChops", 500.0, "trees"),
				Stat("trees_5000", "Deforestation Incident", "5000 trees felled.", "TreeChops", 5000.0, "trees"),
				Stat("mines_1000", "Delver", "1000 nodes mined.", "MineHits", 1000.0, "nodes"),
				Stat("builds_500", "Housebuilder", "500 pieces placed.", "Builds", 500.0, "pieces"),
				Stat("builds_5000", "Architect of the Realm", "5000 pieces placed.", "Builds", 5000.0, "pieces"),
				Stat("crafts_250", "Smith", "250 items crafted or upgraded.", "CraftsOrUpgrades", 250.0, "crafts"),
				Stat("walk_100k", "Long Road", "100 km travelled on foot.", "DistanceWalk", 100000.0, "km", 1000.0),
				Stat("sail_50k", "Seafarer", "50 km sailed.", "DistanceSail", 50000.0, "km", 1000.0),
				Stat("portals_100", "Frequent Flyer", "100 portal trips.", "PortalsUsed", 100.0, "trips"),
				Stat("tame_10", "Beastfriend", "10 creatures tamed.", "CreatureTamed", 10.0, "tamed"),
				Stat("food_500", "Well Fed", "500 meals eaten.", "FoodEaten", 500.0, "meals"),
				Stat("sleep_50", "Well Rested", "Slept through 50 nights.", "Sleep", 50.0, "nights"),
				Biome("biome_swamp", "Into the Mire", "Set foot in the Swamp.", "Swamp"),
				Biome("biome_mountain", "Above the Treeline", "Set foot on the Mountain.", "Mountain"),
				Biome("biome_plains", "Golden Fields", "Set foot in the Plains.", "Plains"),
				Biome("biome_mistlands", "Into the Mist", "Set foot in the Mistlands.", "Mistlands"),
				Biome("biome_ashlands", "Where It Burns", "Set foot in the Ashlands.", "AshLands"),
				Biome("biome_deepnorth", "Far North", "Set foot in the Deep North.", "DeepNorth"),
				Playtime("time_24h", "A Day in Valheim", "24 hours on this server.", 24.0),
				Playtime("time_100h", "Resident of the Tenth World", "100 hours on this server.", 100.0),
				Hidden(Stat("turret_death", "Friendly Fire", "Killed by your own turret.", "DeathByTurret", 1.0, "deaths")),
				Hidden(Stat("drawbridge_death", "Architecturally Unfortunate", "Crushed by a drawbridge.", "DeathByDrawBridge", 1.0, "deaths")),
				Hidden(Stat("edge_death", "Off the Map", "Sailed off the edge of the world.", "DeathByEdgeOfWorld", 1.0, "deaths")),
				Hidden(Stat("tree_death", "Timber", "Killed by a tree you were chopping.", "DeathByTree", 1.0, "deaths")),
				Hidden(Stat("petting", "Animal Person", "Petted a tamed animal 100 times.", "TamedPetting", 100.0, "pats"))
			};
		}

		private static AchievementDef Stat(string id, string title, string desc, string stat, double target, string unit, double scale = 1.0)
		{
			return new AchievementDef
			{
				Id = id,
				Title = title,
				Description = desc,
				Type = "stat",
				Stat = stat,
				Target = target,
				Unit = unit,
				Scale = scale
			};
		}

		private static AchievementDef Playtime(string id, string title, string desc, double hours)
		{
			return new AchievementDef
			{
				Id = id,
				Title = title,
				Description = desc,
				Type = "playtime",
				Hours = hours
			};
		}

		private static AchievementDef Biome(string id, string title, string desc, string biome)
		{
			return new AchievementDef
			{
				Id = id,
				Title = title,
				Description = desc,
				Type = "biome",
				Biome = biome
			};
		}

		private static AchievementDef Key(string id, string title, string desc, string key)
		{
			return new AchievementDef
			{
				Id = id,
				Title = title,
				Description = desc,
				Type = "key",
				Key = key
			};
		}

		private static AchievementDef Hidden(AchievementDef def)
		{
			def.Hidden = true;
			return def;
		}
	}
	internal enum AdminListProblem
	{
		Listed,
		NeedsPrefix,
		Empty,
		NearMatch,
		OldFormat,
		Missing
	}
	internal static class AdminHelp
	{
		private static readonly Regex SteamId64 = new Regex("7656119\\d{10}");

		private static readonly Regex OldSteamId = new Regex("^\\s*(STEAM_\\d:\\d:\\d+|\\[U:1:\\d+\\])\\s*$", RegexOptions.IgnoreCase);

		public static string SteamDigits(string id)
		{
			if (string.IsNullOrEmpty(id))
			{
				return null;
			}
			Match match = SteamId64.Match(id);
			if (!match.Success)
			{
				return null;
			}
			return match.Value;
		}

		public static AdminListProblem Diagnose(string connectionId, IList<string> entries)
		{
			return Diagnose(connectionId, null, entries);
		}

		public static AdminListProblem Diagnose(string connectionId, string canonicalId, IList<string> entries)
		{
			List<string> list = new List<string>();
			if (entries != null)
			{
				foreach (string entry in entries)
				{
					if (!string.IsNullOrEmpty(entry) && entry.Trim().Length > 0)
					{
						list.Add(entry);
					}
				}
			}
			if (list.Count == 0)
			{
				return AdminListProblem.Empty;
			}
			if (!string.IsNullOrEmpty(canonicalId))
			{
				foreach (string item in list)
				{
					if (item.Trim() == canonicalId)
					{
						return AdminListProblem.Listed;
					}
				}
			}
			string text = SteamDigits(connectionId);
			if (text != null && !string.IsNullOrEmpty(canonicalId))
			{
				foreach (string item2 in list)
				{
					if (item2.Trim() == text)
					{
						return AdminListProblem.NeedsPrefix;
					}
				}
			}
			if (text != null)
			{
				foreach (string item3 in list)
				{
					if (item3.Contains(text))
					{
						return AdminListProblem.NearMatch;
					}
				}
			}
			foreach (string item4 in list)
			{
				if (OldSteamId.IsMatch(item4))
				{
					return AdminListProblem.OldFormat;
				}
			}
			return AdminListProblem.Missing;
		}

		public static List<string> Explain(string connectionId, AdminListProblem problem)
		{
			return Explain(connectionId, null, problem);
		}

		public static List<string> Explain(string connectionId, string canonicalId, AdminListProblem problem)
		{
			string text = SteamDigits(connectionId);
			string text2 = ((!string.IsNullOrEmpty(canonicalId)) ? canonicalId : (text ?? connectionId));
			List<string> list = new List<string> { "The server sees your connection as: " + connectionId };
			switch (problem)
			{
			case AdminListProblem.Empty:
				list.Add("The adminlist.txt the server reads has no IDs in it. It uses the one in its save folder, next to worlds_local, not one in the install folder.");
				break;
			case AdminListProblem.Listed:
				list.Add("adminlist.txt already lists exactly that, so this should have worked. Restart the server so the list is read again, and check you are not connecting with a different account.");
				break;
			case AdminListProblem.NeedsPrefix:
				list.Add("Your ID is in adminlist.txt as the bare number, but this version of Valheim wants the prefixed form. Replace that line with exactly: " + text2);
				break;
			case AdminListProblem.NearMatch:
				list.Add("adminlist.txt does list you, but with other characters on the same line (spaces, quotes, a comment). Put " + text2 + " alone on its own line.");
				break;
			case AdminListProblem.OldFormat:
				list.Add("adminlist.txt has an old-style Steam ID (STEAM_0:... or [U:1:...]). Valheim needs the 17-digit one" + ((text != null) ? (": " + text) : "") + ".");
				break;
			default:
				list.Add("Add " + text2 + " on its own line to adminlist.txt in the server's save folder, next to worlds_local.");
				break;
			}
			list.Add("The server picks up changes to adminlist.txt straight away, no restart needed.");
			return list;
		}
	}
	internal static class Announcer
	{
		private static readonly HashSet<long> ModdedPeers = new HashSet<long>();

		public static void MarkModded(long peerUid)
		{
			ModdedPeers.Add(peerUid);
		}

		public static void Retain(HashSet<long> connected)
		{
			ModdedPeers.RemoveWhere((long uid) => !connected.Contains(uid));
		}

		public static void Reset()
		{
			ModdedPeers.Clear();
		}

		public static void Public(string text)
		{
			SkaldPlugin.Log.LogInfo((object)("Announce: " + text));
			if (SkaldConfig.AnnounceInChat.Value)
			{
				Broadcast(ShowStyle.Chat, text, null);
			}
		}

		public static void Banner(string banner, string detail)
		{
			SkaldPlugin.Log.LogInfo((object)("Banner: " + banner + " | " + detail));
			if (SkaldConfig.AnnounceInChat.Value)
			{
				Broadcast(ShowStyle.Banner, banner, detail);
			}
		}

		public static void Private(long peerUid, string text)
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			if (instance.GetPeer(peerUid) == null)
			{
				if (SkaldRuntime.HasLocalPlayer)
				{
					ClientDisplay.Show(ShowStyle.Private, Label(), text, null);
				}
			}
			else
			{
				Send(peerUid, ShowStyle.Private, text, null);
			}
		}

		public static void PlayerUnlock(string playerName, Achievement achievement, bool firstToReach)
		{
			string text = (string.IsNullOrEmpty(playerName) ? "Someone" : playerName);
			string detail = (firstToReach ? (text + " is the first on the server to set foot in the " + achievement.BiomeName + ".") : achievement.Description);
			Banner(text + " earned \"" + achievement.Title + "\"", detail);
		}

		public static void WorldUnlock(Achievement achievement)
		{
			Banner(achievement.Title, achievement.Description);
		}

		private static void Broadcast(ShowStyle style, string primary, string secondary)
		{
			foreach (long moddedPeer in ModdedPeers)
			{
				Send(moddedPeer, style, primary, secondary);
			}
			if (SkaldRuntime.HasLocalPlayer)
			{
				ClientDisplay.Show(style, Label(), primary, secondary);
			}
		}

		private static void Send(long peerUid, ShowStyle style, string primary, string secondary)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			ZRoutedRpc instance = ZRoutedRpc.instance;
			if (instance != null)
			{
				ZPackage val = new ZPackage();
				val.Write(3);
				val.Write((int)style);
				val.Write(Label());
				val.Write(primary ?? "");
				val.Write(secondary ?? "");
				instance.InvokeRoutedRPC(peerUid, "Skald_Show", new object[1] { val });
			}
		}

		private static string Label()
		{
			return SafeText.Clean(SkaldConfig.AnnouncerName.Value, 24);
		}
	}
	internal static class SafeText
	{
		public static string Clean(string value, int maxLength)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(value.Length);
			foreach (char c in value)
			{
				if (!char.IsControl(c) && c != '<' && c != '>')
				{
					stringBuilder.Append(c);
					if (stringBuilder.Length >= maxLength)
					{
						break;
					}
				}
			}
			return stringBuilder.ToString().Trim();
		}
	}
	internal static class BossFights
	{
		private const float CasualtyRadius = 120f;

		private const double MaxFightSeconds = 7200.0;

		private const double UnknownFightWindow = 1800.0;

		private static readonly Dictionary<string, long> RecentlyReported = new Dictionary<string, long>();

		public static void OnBossDown(long sender, ZPackage pkg)
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
			if (!SkaldConfig.BossSummaries.Value || !SkaldStore.Loaded || !Throttle.Allow(sender, "boss", 10f))
			{
				return;
			}
			string key;
			string text;
			double num;
			Vector3 pos;
			try
			{
				if (pkg.ReadInt() != 3)
				{
					return;
				}
				key = SafeText.Clean(pkg.ReadString(), 60);
				text = SafeText.Clean(pkg.ReadString(), 40);
				num = pkg.ReadDouble();
				pos = new Vector3(pkg.ReadSingle(), pkg.ReadSingle(), pkg.ReadSingle());
			}
			catch (Exception ex)
			{
				SkaldPlugin.Log.LogError((object)("Bad Skald boss report: " + ex.Message));
				return;
			}
			long num2 = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			if (!RecentlyReported.TryGetValue(key, out var value) || num2 - value >= 60)
			{
				RecentlyReported[key] = num2;
				bool flag = num > 0.0 && num <= 7200.0;
				double num3 = (flag ? num : 1800.0);
				List<string> dead = ServerHub.DeathsNear(pos, 120f, num2 - (long)num3 - 15);
				string text2 = Describe(string.IsNullOrEmpty(text) ? "The boss" : text, flag ? num : (-1.0), dead);
				Announcer.Public(text2);
				if (SkaldConfig.DiscordAnnounceAchievements.Value)
				{
					DiscordNotifier.Alert(AlertLevel.Info, "Boss down", text2);
				}
			}
		}

		public static string Describe(string boss, double seconds, List<string> dead)
		{
			string text = ((seconds > 0.0) ? (boss + " fell after a " + Duration(seconds) + " fight.") : (boss + " has fallen."));
			if (dead.Count == 0)
			{
				return text + " No one died. Suspicious.";
			}
			return text + " Casualties: " + dead.Count + " (" + string.Join(", ", dead) + ").";
		}

		private static string Duration(double seconds)
		{
			if (seconds < 90.0)
			{
				return Math.Max(1, (int)Math.Round(seconds)) + "-second";
			}
			if (seconds < 5400.0)
			{
				return (int)Math.Round(seconds / 60.0) + "-minute";
			}
			return (int)Math.Round(seconds / 3600.0) + "-hour";
		}
	}
	internal static class CatalogFile
	{
		private const string Header = "// Skald achievements. Edit freely; the server reads this at startup.\n//\n// Every entry needs: id, title, description, type.\n//   \"type\": \"stat\"     - stat (a PlayerStatType name), target, unit, optional scale\n//                        scale divides the numbers shown, e.g. 1000 for metres -> km\n//   \"type\": \"playtime\" - hours on this server\n//   \"type\": \"biome\"    - biome: Meadows, BlackForest, Swamp, Mountain, Plains,\n//                        Mistlands, AshLands, DeepNorth, Ocean\n//   \"type\": \"key\"      - key: a world flag, e.g. defeated_bonemass (server-wide)\n//\n// Optional on any entry: \"hidden\": true, to keep it off the list until earned.\n//\n// Stat achievements count progress made on THIS server, from the moment Skald\n// first saw the character. Delete this file to get the defaults back.\n";

		public static string PathFor()
		{
			return Path.Combine(Path.Combine(Paths.ConfigPath, "Skald"), "achievements.json");
		}

		public static List<Achievement> Load()
		{
			string text = PathFor();
			List<string> list = new List<string>();
			try
			{
				if (!File.Exists(text))
				{
					WriteDefaults(text);
					SkaldPlugin.Log.LogInfo((object)("Wrote the default achievements to " + text + ". Edit it to add your own."));
					return Compile(AchievementCatalog.Defaults(), list);
				}
				List<Achievement> list2 = Compile(ParseFile(File.ReadAllText(text), list), list);
				foreach (string item in list)
				{
					SkaldPlugin.Log.LogError((object)("achievements.json: " + item));
				}
				if (list2.Count == 0)
				{
					SkaldPlugin.Log.LogError((object)"achievements.json produced no usable achievements; using the built-in catalog for this session. The file has not been changed.");
					return Compile(AchievementCatalog.Defaults(), new List<string>());
				}
				SkaldPlugin.Log.LogInfo((object)("Loaded " + list2.Count + " achievements from " + text + ((list.Count > 0) ? (" (" + list.Count + " skipped, see above)") : "")));
				return list2;
			}
			catch (Exception ex)
			{
				SkaldPlugin.Log.LogError((object)("Could not read " + text + " (" + ex.Message + "); using the built-in catalog for this session. The file has not been changed."));
				return Compile(AchievementCatalog.Defaults(), new List<string>());
			}
		}

		public static List<AchievementDef> ParseFile(string text, List<string> errors)
		{
			JsonValue jsonValue = Json.Parse(text);
			JsonValue jsonValue2;
			if (jsonValue.Type == JsonValue.Kind.Array)
			{
				jsonValue2 = jsonValue;
			}
			else
			{
				if (!jsonValue.Has("achievements"))
				{
					throw new JsonException("Expected an object with an \"achievements\" array.");
				}
				jsonValue2 = jsonValue.Get("achievements");
			}
			if (jsonValue2.Type != JsonValue.Kind.Array)
			{
				throw new JsonException("\"achievements\" must be an array.");
			}
			List<AchievementDef> list = new List<AchievementDef>();
			for (int i = 0; i < jsonValue2.Array.Count; i++)
			{
				JsonValue jsonValue3 = jsonValue2.Array[i];
				if (jsonValue3.Type != JsonValue.Kind.Object)
				{
					errors.Add("entry " + (i + 1) + " is not an object.");
					continue;
				}
				list.Add(new AchievementDef
				{
					Id = jsonValue3.GetString("id"),
					Title = jsonValue3.GetString("title"),
					Description = jsonValue3.GetString("description", ""),
					Type = (jsonValue3.GetString("type") ?? "").ToLowerInvariant(),
					Stat = jsonValue3.GetString("stat"),
					Target = jsonValue3.GetNumber("target"),
					Unit = jsonValue3.GetString("unit", ""),
					Scale = jsonValue3.GetNumber("scale", 1.0),
					Hours = jsonValue3.GetNumber("hours"),
					Biome = jsonValue3.GetString("biome"),
					Key = jsonValue3.GetString("key"),
					Hidden = jsonValue3.GetBool("hidden")
				});
			}
			return list;
		}

		public static List<Achievement> Compile(List<AchievementDef> defs, List<string> errors)
		{
			//IL_014a: 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_0375: Unknown result type (might be due to invalid IL or missing references)
			//IL_037c: Expected I4, but got Unknown
			//IL_03a0: Unknown result type (might be due to invalid IL or missing references)
			List<Achievement> list = new List<Achievement>();
			HashSet<string> hashSet = new HashSet<string>();
			foreach (AchievementDef def in defs)
			{
				if (string.IsNullOrEmpty(def.Id))
				{
					errors.Add("an entry has no id; skipped.");
					continue;
				}
				if (!hashSet.Add(def.Id))
				{
					errors.Add("'" + def.Id + "' appears more than once; the later one is skipped.");
					continue;
				}
				Achievement achievement = new Achievement
				{
					Id = def.Id,
					Title = (string.IsNullOrEmpty(def.Title) ? def.Id : def.Title),
					Description = (def.Description ?? ""),
					Hidden = def.Hidden,
					Scope = AchievementScope.Player
				};
				switch (def.Type)
				{
				case "stat":
				{
					if (string.IsNullOrEmpty(def.Stat) || !Enum.TryParse<PlayerStatType>(def.Stat, ignoreCase: true, out PlayerStatType stat) || !Enum.IsDefined(typeof(PlayerStatType), stat))
					{
						errors.Add("'" + def.Id + "' has an unknown stat '" + def.Stat + "'; skipped.");
						continue;
					}
					if (def.Target <= 0.0)
					{
						errors.Add("'" + def.Id + "' needs a target above zero; skipped.");
						continue;
					}
					double target = def.Target;
					achievement.PlayerTest = (PlayerRecord rec) => (double)rec.Progress(stat) >= target;
					achievement.Current = (PlayerRecord rec) => rec.Progress(stat);
					achievement.Target = target;
					achievement.Unit = def.Unit ?? "";
					achievement.DisplayScale = ((def.Scale > 0.0) ? def.Scale : 1.0);
					break;
				}
				case "playtime":
				{
					if (def.Hours <= 0.0)
					{
						errors.Add("'" + def.Id + "' needs hours above zero; skipped.");
						continue;
					}
					double seconds = def.Hours * 3600.0;
					achievement.PlayerTest = (PlayerRecord rec) => rec.PlaytimeSeconds >= seconds;
					achievement.Current = (PlayerRecord rec) => rec.PlaytimeSeconds;
					achievement.Target = seconds;
					achievement.Unit = "h";
					achievement.DisplayScale = 3600.0;
					break;
				}
				case "biome":
				{
					if (string.IsNullOrEmpty(def.Biome) || !Enum.TryParse<Biome>(def.Biome, ignoreCase: true, out Biome result) || (int)result == 0)
					{
						errors.Add("'" + def.Id + "' has an unknown biome '" + def.Biome + "'; skipped.");
						continue;
					}
					int biomeId = (int)result;
					achievement.PlayerTest = (PlayerRecord rec) => rec.Biomes.Contains(biomeId);
					achievement.BiomeId = biomeId;
					achievement.BiomeName = Pretty(result);
					break;
				}
				case "key":
				{
					if (string.IsNullOrEmpty(def.Key))
					{
						errors.Add("'" + def.Id + "' needs a key; skipped.");
						continue;
					}
					string key = def.Key;
					achievement.Scope = AchievementScope.World;
					achievement.WorldTest = (WorldRecord world) => world.GlobalKeys.Contains(key);
					break;
				}
				default:
					errors.Add("'" + def.Id + "' has unknown type '" + def.Type + "'; expected stat, playtime, biome or key. Skipped.");
					continue;
				}
				list.Add(achievement);
			}
			return list;
		}

		private unsafe static string Pretty(Biome biome)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Invalid comparison between Unknown and I4
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Invalid comparison between Unknown and I4
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Invalid comparison between Unknown and I4
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Invalid comparison between Unknown and I4
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Invalid comparison between Unknown and I4
			if ((int)biome <= 8)
			{
				if ((int)biome == 4)
				{
					return "Mountains";
				}
				if ((int)biome == 8)
				{
					return "Black Forest";
				}
			}
			else
			{
				if ((int)biome == 32)
				{
					return "Ashlands";
				}
				if ((int)biome == 64)
				{
					return "Deep North";
				}
			}
			return ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString();
		}

		public static void WriteDefaults(string path)
		{
			string directoryName = Path.GetDirectoryName(path);
			if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			File.WriteAllText(path, Serialize(AchievementCatalog.Defaults()));
		}

		public static string Serialize(List<AchievementDef> defs)
		{
			JsonValue jsonValue = JsonValue.NewArray();
			foreach (AchievementDef def in defs)
			{
				JsonValue jsonValue2 = JsonValue.NewObject();
				jsonValue2.Set("id", JsonValue.Of(def.Id));
				jsonValue2.Set("title", JsonValue.Of(def.Title));
				jsonValue2.Set("description", JsonValue.Of(def.Description ?? ""));
				jsonValue2.Set("type", JsonValue.Of(def.Type));
				switch (def.Type)
				{
				case "stat":
					jsonValue2.Set("stat", JsonValue.Of(def.Stat));
					jsonValue2.Set("target", JsonValue.Of(def.Target));
					if (!string.IsNullOrEmpty(def.Unit))
					{
						jsonValue2.Set("unit", JsonValue.Of(def.Unit));
					}
					if (def.Scale != 1.0)
					{
						jsonValue2.Set("scale", JsonValue.Of(def.Scale));
					}
					break;
				case "playtime":
					jsonValue2.Set("hours", JsonValue.Of(def.Hours));
					break;
				case "biome":
					jsonValue2.Set("biome", JsonValue.Of(def.Biome));
					break;
				case "key":
					jsonValue2.Set("key", JsonValue.Of(def.Key));
					break;
				}
				if (def.Hidden)
				{
					jsonValue2.Set("hidden", JsonValue.Of(b: true));
				}
				jsonValue.Add(jsonValue2);
			}
			JsonValue jsonValue3 = JsonValue.NewObject();
			jsonValue3.Set("achievements", jsonValue);
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("// Skald achievements. Edit freely; the server reads this at startup.\n//\n// Every entry needs: id, title, description, type.\n//   \"type\": \"stat\"     - stat (a PlayerStatType name), target, unit, optional scale\n//                        scale divides the numbers shown, e.g. 1000 for metres -> km\n//   \"type\": \"playtime\" - hours on this server\n//   \"type\": \"biome\"    - biome: Meadows, BlackForest, Swamp, Mountain, Plains,\n//                        Mistlands, AshLands, DeepNorth, Ocean\n//   \"type\": \"key\"      - key: a world flag, e.g. defeated_bonemass (server-wide)\n//\n// Optional on any entry: \"hidden\": true, to keep it off the list until earned.\n//\n// Stat achievements count progress made on THIS server, from the moment Skald\n// first saw the character. Delete this file to get the defaults back.\n");
			stringBuilder.Append(Json.Write(jsonValue3));
			stringBuilder.Append('\n');
			return stringBuilder.ToString();
		}
	}
	internal static class Catchup
	{
		private const int MaxNamedAchievements = 4;

		public static List<string> Build(PlayerRecord rec, WorldRecord world, ICollection<PlayerRecord> players, long since, long now, double dayLength)
		{
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			Dictionary<string, Achievement> titles = new Dictionary<string, Achievement>();
			foreach (Achievement item in AchievementCatalog.All)
			{
				titles[item.Id] = item;
			}
			List<string> list3 = (from kv in world.UnlockTimes
				where kv.Value >= since && titles.ContainsKey(kv.Key)
				orderby kv.Value
				select titles[kv.Key].Description.Replace(" has fallen.", "")).ToList();
			if (list3.Count > 0)
			{
				list2.Add(((list3.Count == 1) ? "Boss down: " : "Bosses down: ") + string.Join(", ", list3) + ".");
			}
			List<string> list4 = new List<string>();
			foreach (PlayerRecord player in players)
			{
				if (player.PlayerId == rec.PlayerId)
				{
					continue;
				}
				foreach (KeyValuePair<string, long> item2 in player.UnlockTimes.OrderBy((KeyValuePair<string, long> k) => k.Value))
				{
					if (item2.Value >= since && titles.ContainsKey(item2.Key) && titles[item2.Key].Scope == AchievementScope.Player)
					{
						list4.Add(player.Name + " earned " + titles[item2.Key].Title);
					}
				}
			}
			if (list4.Count > 0)
			{
				List<string> list5 = list4.Take(4).ToList();
				string text = string.Join(", ", list5);
				if (list4.Count > list5.Count)
				{
					text = text + " and " + (list4.Count - list5.Count) + " more";
				}
				list2.Add(text + ".");
			}
			List<DeathEvent> list6 = world.Deaths.Where((DeathEvent d) => d.Time >= since).ToList();
			if (list6.Count > 0)
			{
				IGrouping<string, DeathEvent> grouping = (from d in list6
					group d by (!string.IsNullOrEmpty(d.KillerName)) ? d.KillerName : DeathCauses.Name(d.HitType) into g
					orderby g.Count() descending
					select g).First();
				list2.Add(Phrases.Plural(list6.Count, "death") + " while you were gone" + ((grouping.Count() > 1) ? (" (" + grouping.Key + " took " + grouping.Count() + ")") : "") + ".");
			}
			int num = Streaks.Days(rec.StreakWorldSeconds, dayLength);
			if (num >= 1)
			{
				list2.Add("Your " + Phrases.Plural(num, "day") + " without dying still stands.");
			}
			if (list2.Count == 0)
			{
				return list;
			}
			list.Add("While you were away (" + Away(now - since) + "):");
			list.AddRange(list2);
			return list;
		}

		public static string Away(long seconds)
		{
			if (seconds < 3600)
			{
				return Phrases.Plural((int)Math.Max(1L, seconds / 60), "minute");
			}
			if (seconds < 86400)
			{
				return Phrases.Plural((int)Math.Round((double)seconds / 3600.0), "hour");
			}
			return Phrases.Plural((int)Math.Round((double)seconds / 86400.0), "day");
		}
	}
	internal static class Commands
	{
		public static void OnCommand(long sender, ZPackage pkg)
		{
			if (!SkaldConfig.ChatCommands.Value || !Throttle.Allow(sender, "command", 1f))
			{
				return;
			}
			string text;
			try
			{
				if (pkg.ReadInt() != 3)
				{
					return;
				}
				text = SafeText.Clean(pkg.ReadString(), 120);
			}
			catch (Exception ex)
			{
				SkaldPlugin.Log.LogError((object)("Bad Skald command: " + ex.Message));
				return;
			}
			if (!SkaldStore.Loaded)
			{
				Announcer.Private(sender, "Skald is still waking up. Try again in a moment.");
				return;
			}
			PlayerRecord self = ServerHub.RecordForSender(sender);
			bool flag = ServerHub.IsAdminSender(sender);
			List<string> list;
			try
			{
				list = Handle(text, self, flag, ServerHub.DayLength(), ServerHub.RecapPreview, ServerHub.WorldReport);
			}
			catch (Exception ex2)
			{
				SkaldPlugin.Log.LogError((object)("Skald command '" + text + "' failed: " + ex2));
				list = new List<string> { "Something went wrong answering that. It has been logged." };
			}
			if (!flag && text.TrimStart().StartsWith("!recap", StringComparison.OrdinalIgnoreCase))
			{
				list.AddRange(ServerHub.AdminDiagnosis(sender));
			}
			foreach (string item in list)
			{
				Announcer.Private(sender, item);
			}
		}

		public static List<string> Handle(string text, PlayerRecord self, bool isAdmin, double dayLength, Func<List<string>> recapPreview, Func<List<string>> worldReport = null)
		{
			string[] array = (text ?? "").Trim().Split(new char[1] { ' ' }, 2, StringSplitOptions.RemoveEmptyEntries);
			string text2 = ((array.Length != 0) ? array[0].TrimStart('!').ToLowerInvariant() : "");
			string text3 = ((array.Length > 1) ? array[1].Trim() : "");
			if (text2 == "skald" && string.Equals(text3, "help", StringComparison.OrdinalIgnoreCase))
			{
				return Help();
			}
			switch (text2)
			{
			case "skald":
			{
				PlayerRecord playerRecord = Target(self, text3);
				if (playerRecord != null)
				{
					return Summary(playerRecord, dayLength);
				}
				return NotFound(text3);
			}
			case "deaths":
			{
				PlayerRecord playerRecord2 = Target(self, text3);
				if (playerRecord2 != null)
				{
					return Deaths(playerRecord2);
				}
				return NotFound(text3);
			}
			case "top":
				return Top(SkaldStore.Players.Values, dayLength);
			case "world":
				if (worldReport == null)
				{
					return new List<string> { "The world census is not available." };
				}
				return worldReport();
			case "recap":
				if (!isAdmin)
				{
					return new List<string> { "Only server admins can use !recap." };
				}
				return recapPreview();
			default:
				return Help();
			}
		}

		public static List<string> Help()
		{
			return new List<string> { "!skald [name] - achievements, playtime, streak and what's next", "!deaths [name] - how, and to what", "!top - leaderboards", "!world - what the world is made of, and what grew this week", "!recap - post this week's recap so far to Discord (admins)" };
		}

		public static List<string> Summary(PlayerRecord rec, double dayLength)
		{
			List<Achievement> list = AchievementCatalog.All.Where((Achievement a) => a.Scope == AchievementScope.Player).ToList();
			int num = list.Count((Achievement a) => rec.Unlocked.Contains(a.Id));
			int count = SkaldStore.World.Unlocked.Count;
			int num2 = AchievementCatalog.All.Count - list.Count;
			List<string> list2 = new List<string>();
			list2.Add(rec.Name + " - " + num + "/" + list.Count + " achievements · " + Hours(rec.PlaytimeSeconds) + " played · " + Phrases.Plural(rec.DeathCount, "death") + " · server bosses " + count + "/" + num2);
			List<string> list3 = list2;
			int n = Streaks.Days(rec.StreakWorldSeconds, dayLength);
			int num3 = Streaks.Days(rec.BestStreakWorldSeconds, dayLength);
			string text = "Streak: " + Phrases.Plural(n, "day") + " (best " + num3 + ")";
			if (rec.Nemeses.Count > 0)
			{
				text = text + " · Nemesis: " + string.Join(", ", rec.Nemeses.Select((string k) => KillerName(rec, k)));
			}
			list3.Add(text);
			List<string> list4 = (from x in (from a in list
					where !rec.Unlocked.Contains(a.Id) && a.Current != null && a.Target > 0.0
					select new
					{
						a = a,
						ratio = a.Current(rec) / a.Target
					} into x
					orderby x.ratio descending
					select x).Take(3)
				select x.a.Title + " " + Amount(x.a, x.a.Current(rec)) + "/" + Amount(x.a, x.a.Target) + ((x.a.Unit.Length > 0) ? (" " + x.a.Unit) : "")).ToList();
			if (list4.Count > 0)
			{
				list3.Add("Next up: " + string.Join(" · ", list4));
			}
			return list3;
		}

		public static List<string> Deaths(PlayerRecord rec)
		{
			if (rec.DeathCount == 0)
			{
				return new List<string> { rec.Name + " has not died on this server. Yet." };
			}
			List<string> list = new List<string> { rec.Name + " has died " + Phrases.Plural(rec.DeathCount, "time") + "." };
			List<string> list2 = (from kv in rec.DeathsByCause.OrderByDescending((KeyValuePair<int, int> kv) => kv.Value).Take(4)
				select DeathCauses.Name(kv.Key) + " " + kv.Value).ToList();
			if (list2.Count > 0)
			{
				list.Add("Causes: " + string.Join(" · ", list2));
			}
			List<string> list3 = (from kv in rec.KillerDeaths.OrderByDescending((KeyValuePair<string, int> kv) => kv.Value).Take(4)
				select KillerName(rec, kv.Key) + " " + kv.Value).ToList();
			if (list3.Count > 0)
			{
				list.Add("Killers: " + string.Join(" · ", list3));
			}
			list.Add((rec.Nemeses.Count == 0) ? "No active grudges." : ("Nemesis: " + string.Join(", ", rec.Nemeses.Select(delegate(string k)
			{
				rec.NemesisCount.TryGetValue(k, out var value);
				return KillerName(rec, k) + " (" + value + " since the last reckoning)";
			}))));
			return list;
		}

		public static List<string> Top(IEnumerable<PlayerRecord> all, double dayLength)
		{
			List<PlayerRecord> list = all.Where((PlayerRecord p) => !string.IsNullOrEmpty(p.Name)).ToList();
			if (list.Count == 0)
			{
				return new List<string> { "Nobody has been tracked yet." };
			}
			int playerAchievements = AchievementCatalog.PlayerCount;
			return new List<string>
			{
				"Most achievements: " + Board(list, (PlayerRecord p) => p.Unlocked.Count, (int n) => n + "/" + playerAchievements),
				"Most time played: " + Board(list, (PlayerRecord p) => p.PlaytimeSeconds, Hours),
				"Most deaths: " + Board(list, (PlayerRecord p) => p.DeathCount, (int n) => n.ToString()),
				"Longest streak: " + Board(list, (PlayerRecord p) => p.BestStreakWorldSeconds, (double s) => Phrases.Plural(Streaks.Days(s, dayLength), "day"))
			};
		}

		private static string Board<T>(List<PlayerRecord> players, Func<PlayerRecord, T> key, Func<T, string> show)
		{
			return string.Join(" · ", from p in players.OrderByDescending(key).Take(3)
				select p.Name + " " + show(key(p)));
		}

		private static PlayerRecord Target(PlayerRecord self, string arg)
		{
			if (!string.IsNullOrEmpty(arg))
			{
				return SkaldStore.FindByName(arg);
			}
			return self;
		}

		private static List<string> NotFound(string arg)
		{
			return new List<string> { string.IsNullOrEmpty(arg) ? "Skald has not seen your character yet. Give it a moment after spawning." : ("No one called '" + arg + "' has been seen here.") };
		}

		private static string KillerName(PlayerRecord rec, string key)
		{
			if (rec.KillerNames.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value))
			{
				return value;
			}
			if (!key.StartsWith("player:"))
			{
				return key;
			}
			return key.Substring(7);
		}

		private static string Hours(double seconds)
		{
			return (seconds / 3600.0).ToString("0.0", CultureInfo.InvariantCulture) + " h";
		}

		private static string Amount(Achievement a, double raw)
		{
			double d = raw / a.DisplayScale;
			if (!(a.DisplayScale > 1.0))
			{
				return Math.Floor(d).ToString("0", CultureInfo.InvariantCulture);
			}
			return d.ToString("0.#", CultureInfo.InvariantCulture);
		}
	}
	internal static class DeathMessages
	{
		private static readonly string[] Fallback = new string[10] { "{player} died in a way the sagas decline to record.", "{player} stopped living, abruptly and without ceremony.", "Something unspeakable happened to {player}. The details are missing.", "{player} was returned to the soil by unclear means.", "{player} has been erased from the working day.", "{player} died in a manner the skalds refuse to describe.", "{player} stopped, for reasons unrecorded.", "Something got {player}. Nobody knows what.", "{player} has left the world by an unclear exit.", "{player} is dead. The details did not survive either." };

		private static readonly Dictionary<HitType, string[]> Catalog = new Dictionary<HitType, string[]>
		{
			{
				(HitType)1,
				new string[40]
				{
					"{player} was opened from collarbone to hip by {killer}.", "{killer} beat {player} into a shape no helmet was designed for.", "{player} was murdered by {killer}, thoroughly and with evident enthusiasm.", "{killer} removed {player} from the inside out and left the packaging.", "{player} was folded backwards by {killer} until something important gave way.", "{killer} introduced {player} to their own skeleton.", "{player} was chewed, considered, and discarded by {killer}.", "{killer} rearranged {player} into an abstract piece.", "{player} was hollowed out by {killer} with unsettling precision.", "{killer} pulped {player} into something the crows will argue over.",
					"{player} came apart in {killer}'s grip like wet bread.", "{killer} struck {player} hard enough to redistribute them across the biome.", "{player} was disassembled by {killer} in the wrong order.", "{killer} made a long, wet, conclusive point about {player}.", "{player} was unmade by {killer} and is now mostly a smell.", "{killer} took {player} apart with the patience of a butcher.", "{player} was reduced by {killer} to a cautionary tale and a stain.", "{killer} hit {player} so hard the armour kept the shape and the rest did not.", "{player} was emptied out by {killer} in about three seconds.", "{killer} broke {player} into pieces that no longer fit together.",
					"{player} went down under {killer} and did not come back up.", "{killer} turned {player} inside out and left the result for the ravens.", "{player} was ended by {killer} mid-sentence.", "{killer} put {player} through something that will require a mop.", "{player} was flattened, opened and finished by {killer}, in that order.", "{killer} made {player} a lesson in what not to approach.", "{player} was separated from several important parts by {killer}.", "{killer} closed the distance, and {player} closed permanently.", "{player} was struck down by {killer} and left to cool.", "{killer} spent four seconds on {player}. It was enough.",
					"{player} became something {killer} had to wipe off.", "{killer} hit {player} once. That was the whole fight.", "{player} was dismantled by {killer} with no particular urgency.", "{killer} ground {player} into the moss.", "{player} was carved up by {killer} and arranged badly.", "{killer} left {player} in a shape the healers would refuse.", "{player} met {killer} and lost every argument at once.", "{killer} turned {player} into an open question.", "{player} was punched through by {killer}, and kept going briefly.", "{killer} ended {player} with the sound of something wet giving way."
				}
			},
			{
				(HitType)2,
				new string[16]
				{
					"{killer} murdered {player}. In cold blood. On purpose.", "{player} was cut down by {killer}, who will have to explain that later.", "{killer} put {player} in the ground and kept the axe.", "{player} learned that {killer} was never really a friend.", "{killer} split {player} open and called it a misunderstanding.", "{player} was betrayed, perforated, and left steaming by {killer}.", "{killer} ended {player} with the confidence of someone who planned it.", "{player} died of {killer}, at close range.", "{killer} killed {player}. The longhouse will hear about this.", "{player} was put down by {killer}. A friend, allegedly.",
					"{killer} buried an axe in {player} and called it a discussion.", "{player} died to {killer}. The word for that is murder.", "{killer} took {player} apart, and took the loot too.", "{player} trusted {killer}. That was the mistake.", "{killer} opened {player} up and looked genuinely pleased about it.", "{player} was killed by {killer}. Not a monster. A friend."
				}
			},
			{
				(HitType)3,
				new string[18]
				{
					"{player} fell a very long way and arrived all at once.", "{player} discovered the ground travelling upward at speed.", "{player} hit the earth hard enough to become part of it.", "{player} was flattened into a thin, regrettable layer.", "{player} came down wrong and then kept being wrong.", "Gravity took an interest in {player} and won decisively.", "{player} landed in several places at once.", "{player} broke every bone worth naming, then a few extra.", "{player} stepped off something high and learned about it.", "{player} arrived at the bottom in worse condition than expected.",
					"{player} tested the drop personally. The results were conclusive.", "{player} fell, and the landing did all the work.", "{player} went down the fast way and stopped hard.", "{player} misjudged one step and paid for all of them.", "{player} discovered that the ground does not negotiate.", "{player} fell far enough to have time to think about it.", "{player} hit the rocks and became a geological feature.", "{player} took the shortest way down and the longest way back."
				}
			},
			{
				(HitType)4,
				new string[12]
				{
					"{player} filled up with cold water and stopped arguing.", "{player} breathed the sea and the sea accepted.", "{player} sank quietly, which was the loudest part.", "The water went into {player} and never came out.", "{player} drowned, slowly, with plenty of time to think about it.", "{player} is now part of the lake.", "{player} swam out too far and stopped swimming back.", "{player} went down with the armour still on.", "{player} traded air for water at an unfavourable rate.", "{player} tried to out-swim a bad decision and lost.",
					"{player} disappeared under the surface without much noise.", "{player} learned that stamina runs out before the sea does."
				}
			},
			{
				(HitType)5,
				new string[12]
				{
					"{player} cooked inside their own armour.", "{player} burned down to the interesting parts.", "{player} went up like dry kindling and stayed up.", "{player} was rendered, in the culinary sense.", "Fire ate {player} from the skin inward.", "{player} is now a smell and a warning.", "{player} caught fire and stayed caught.", "{player} burned until there was nothing left worth naming.", "{player} became a torch, briefly and brightly.", "{player} was cooked through, and then some.",
					"{player} stood too close to something hot and hungry.", "{player} went out in flames, and then just went out."
				}
			},
			{
				(HitType)6,
				new string[12]
				{
					"{player} froze solid with their eyes still open.", "The cold got into {player} and turned everything to glass.", "{player} stiffened, cracked, and stopped.", "{player} was preserved at the exact moment of realising.", "Frost hollowed {player} out and left the shell standing.", "{player} went blue, then quiet.", "{player} stopped shivering, which was the bad sign.", "{player} was taken by the cold, quietly and completely.", "{player} froze to the mountain and stayed there.", "{player} forgot a cape, and the mountain remembered.",
					"{player} got cold, then colder, then permanent.", "The frost put {player} away for good."
				}
			},
			{
				(HitType)7,
				new string[12]
				{
					"{player} rotted from the middle outward.", "{player} was liquefied by something that got in through a scratch.", "Poison walked through {player} and closed the door behind it.", "{player} turned an unhelpful colour and then a worse one.", "{player} died sweating, twitching, and deeply regretting the swamp.", "Something venomous had a slow and thorough opinion about {player}.", "{player} died green and unhappy.", "{player} was hollowed out by something with a taste for it.", "{player} went stiff, and then went still.", "{player} was poisoned thoroughly, by something small.",
					"{player} spent the last few minutes regretting the swamp.", "Venom finished what a bad idea started for {player}."
				}
			},
			{
				(HitType)9,
				new string[8] { "{player} suffocated in their own hearth smoke.", "{player} was kippered inside their own house.", "{player} choked to death somewhere that should have been safe.", "The smoke found {player} and would not leave.", "{player} built a fire indoors and paid the standard price.", "{player} was smoked out of a house {player} built.", "{player} choked on hearth smoke like an amateur.", "{player} died of poor ventilation. A truly Viking end." }
			},
			{
				(HitType)8,
				new string[8] { "{player} was swallowed by the water without fuss.", "The sea took {player} and did not negotiate.", "{player} went under and the surface closed over neatly.", "{player} is somewhere down there, still.", "{player} went into the water, and the water kept the difference.", "{player} was taken quietly, without a splash worth mentioning.", "{player} sank out of the story.", "The water closed over {player} and moved on." }
			},
			{
				(HitType)10,
				new string[8] { "{player} sailed off the edge of everything.", "{player} found where the world stops, and kept going.", "{player} was deleted by geography.", "{player} fell out of the map and out of the story.", "{player} kept sailing until there was nothing left to sail on.", "{player} tested the edge of the world. The world won.", "{player} went past the last wave and did not come back.", "{player} fell off the end of everything." }
			},
			{
				(HitType)11,
				new string[10] { "{player} was struck hard enough to change shape permanently.", "Something enormous met {player} and {player} lost.", "{player} was compressed into a smaller, wetter version.", "{player} took the full force and distributed it poorly.", "{player} was hit so hard the sound arrived afterwards.", "{player} was hit by something with mass and no opinion.", "{player} stopped suddenly against something that did not.", "{player} was compacted.", "{player} took an impact that redistributed the important parts.", "Something big met {player}. Only one of them walked away." }
			},
			{
				(HitType)12,
				new string[8] { "{player} was run down by their own cart.", "{player} was crushed by a wagon they packed themselves.", "{player} lost an argument with a heavy wooden box on wheels.", "The cart went over {player} twice, to be certain.", "{player} was killed by cargo.", "{player} was flattened by a cart, which is a choice.", "{player} lost a wrestling match with a very large wheelbarrow.", "{player} died hauling, which is at least honest work." }
			},
			{
				(HitType)13,
				new string[12]
				{
					"{player} was crushed by the very tree they were chopping.", "A tree fell on {player} with what felt like intent.", "{player} was driven into the forest floor by several tons of pine.", "{player} chopped, looked up, and ran out of time.", "The forest took {player} back in one motion.", "{player} was pressed flat beneath honest timber.", "{player} chopped once too often.", "{player} was buried by the forest, using a single tree.", "{player} was struck down by lumber in transit.", "{player} learned which way a tree falls. Once.",
					"{player} was pressed into the ground by a birch with a grudge.", "{player} died beneath honest work."
				}
			},
			{
				(HitType)14,
				new string[10] { "{player} did this to themselves. Entirely.", "{player} was killed by {player}, which raises questions.", "{player} achieved self-destruction with real commitment.", "{player} took themselves apart and could not finish reassembly.", "{player} needed no help at all, in the end.", "{player} was killed by {player}. No help required.", "{player} managed it alone, which is both impressive and sad.", "{player} did something unwise and then finished the job.", "{player} required no assistance whatsoever.", "{player} is survived by the decision that caused this." }
			},
			{
				(HitType)15,
				new string[10] { "{player} was flattened by their own architecture.", "The building fell on {player}, which the building had been threatening for a while.", "{player} died of poor structural planning.", "{player} was buried under a roof they personally installed.", "{player} learned about load-bearing walls the expensive way.", "{player} was crushed by a build that failed inspection.", "{player} died under a roof {player} was proud of.", "{player} learned about structural integrity from underneath.", "{player} was flattened by an unsupported ambition.", "The house came down. {player} did not get out." }
			},
			{
				(HitType)16,
				new string[8] { "{player} was shot dead by their own defences.", "The turret did not recognise {player} and did not ask.", "{player} was punched through by friendly fire, mechanically applied.", "{player} was perforated by something they built to protect them.", "{player} walked into a firing line of {player}'s own making.", "{player} was shot by a machine that was only following orders.", "{player} died to friendly fire, mechanically administered.", "{player} built it, loaded it, and then stood in front of it." }
			},
			{
				(HitType)17,
				new string[8] { "{player} was crushed between hull and hard place.", "{player} was ground into the seabed by their own longship.", "The boat came down on {player} with the full weight of the voyage.", "{player} was mangled by several tons of buoyant timber.", "{player} was crushed by a boat. On land. Somehow.", "{player} died beneath a vessel {player} was quite fond of.", "{player} lost an argument with several tons of karve.", "{player} was pinned by a hull that had other plans." }
			},
			{
				(HitType)18,
				new string[8] { "{player} was skewered from above by the ceiling.", "A spike of ancient stone went through {player} lengthwise.", "{player} was pinned to the cave floor like a specimen.", "The cave dropped a stone spear on {player} and said nothing.", "{player} was skewered by geology.", "{player} died to a rock with excellent aim.", "{player} was pinned in place by the cave itself.", "{player} looked up too late." }
			},
			{
				(HitType)19,
				new string[8] { "{player} was launched, and landed as several separate deliveries.", "{player} caught a boulder with their entire body.", "{player} was reduced to pulp by long-range artillery.", "Something heavy arrived from far away, and {player} was under it.", "{player} received a boulder, airmail.", "{player} was demolished at range.", "{player} was hit by artillery and scattered accordingly.", "{player} was on the receiving end of very good aim." }
			},
			{
				(HitType)20,
				new string[8] { "{player} was cooked alive in falling ash.", "The burning sky came down on {player}.", "{player} was seared through by cinders and stopped moving.", "{player} caught fire from above and could not put it out.", "{player} burned under a sky that was also burning.", "{player} was set alight from above.", "{player} died in falling fire.", "The ash took {player} while it was still hot." }
			},
			{
				(HitType)21,
				new string[8] { "{player} boiled away in the burning sea.", "{player} went into water that was not water, and did not come back.", "The scalding tide dissolved {player} entirely.", "{player} was rendered down by the Ashlands ocean.", "{player} swam in the wrong sea.", "{player} was boiled by water with opinions.", "{player} went in, and came out as steam.", "{player} was dissolved by the burning tide." }
			},
			{
				(HitType)22,
				new string[8] { "{player} sank into molten rock, briefly and brightly.", "{player} went into the lava and became a light.", "{player} was consumed by liquid stone in under a second.", "The ground opened, glowed, and finished {player}.", "{player} stepped into the glow and became part of it.", "{player} was swallowed by molten stone.", "{player} burned all the way through in one moment.", "{player} is now geology." }
			},
			{
				(HitType)23,
				new string[8] { "{player} was fed into the machine and processed.", "{player} went in whole and did not come out that way.", "The incinerator did not distinguish {player} from refuse.", "{player} was industrially reduced to nothing.", "{player} was processed as waste.", "{player} climbed in. That was the last decision.", "{player} was turned to ash on an industrial schedule.", "The machine did not ask {player} any questions." }
			},
			{
				(HitType)24,
				new string[8] { "{player} was crushed by their own drawbridge.", "{player} was folded into the gate they built.", "The bridge came down, and {player} was in the way.", "{player} was pressed flat by several tons of hinged wood.", "{player} was folded shut.", "{player} died to their own gate, which is embarrassing.", "{player} was caught by a bridge with bad timing.", "{player} was flattened by a door the size of a wall." }
			}
		};

		private static readonly Dictionary<string, string[]> Signature = new Dictionary<string, string[]>
		{
			{
				"deathsquito",
				new string[4] { "A Deathsquito killed {player}. Two grams of pure malice.", "{player} was assassinated by an insect with a name like a warning.", "{killer} arrived at speed and left {player} face down in the Plains.", "{player} heard the buzzing. It was already too late." }
			},
			{
				"troll",
				new string[4] { "{killer} clubbed {player} into the treeline.", "{player} was swatted by {killer} like a bad thought.", "{killer} hit {player} once, and the trees kept the echo.", "{player} was introduced to the ground by {killer}. Repeatedly." }
			},
			{
				"greydwarf",
				new string[4] { "{player} was pulled down by {killer} and friends.", "{killer} threw a rock. {player} stopped a rock.", "{player} was mobbed to death by the forest's least impressive residents.", "{killer} finished {player} with a stick and enthusiasm." }
			},
			{
				"serpent",
				new string[4] { "{killer} took {player} off the deck and into the deep.", "{player} met {killer} at sea and lost the boat and the argument.", "{killer} ended {player}'s sailing career in one bite.", "{player} was dragged under by {killer}, karve and all." }
			},
			{
				"wolf",
				new string[4] { "{killer} ran {player} down in the snow.", "{player} was eaten on a mountainside by {killer}.", "{killer} and the pack left very little of {player}.", "{player} was brought down by teeth and altitude." }
			},
			{
				"goblin",
				new string[4] { "{killer} beat {player} to death with something spiky and homemade.", "{player} was overrun by {killer} and a great deal of shouting.", "{killer} put a torch through {player} and celebrated.", "{player} was taken apart by {killer} in the tall grass." }
			},
			{
				"seeker",
				new string[4] { "{killer} pinned {player} in the mist and did the rest quietly.", "{player} never saw {killer} coming. Nobody does.", "{killer} clicked once, and {player} stopped existing.", "{player} was taken by {killer} in the fog, thoroughly." }
			},
			{
				"gjall",
				new string[4] { "{killer} bombed {player} from above.", "{player} was cooked in the mist by {killer}.", "{killer} dropped something horrible on {player}.", "{player} looked up at {killer}, and that was the last look." }
			},
			{
				"neck",
				new string[4] { "{player} was killed by {killer}. A Neck. Let that sit.", "{killer} finished {player}, which should not be possible.", "{player} died to the least threatening thing in the Tenth World.", "{killer} will be telling this story for generations." }
			},
			{
				"boar",
				new string[4] { "{player} was gored by {killer}. Breakfast won.", "{killer} killed {player}. In fairness, it was provoked.", "{player} lost to livestock.", "{killer} put {player} down in the meadows, humiliatingly." }
			},
			{
				"skeleton",
				new string[4] { "{killer} put rusted iron through {player}.", "{player} was killed by something that was already dead.", "{killer} rattled up out of the dark and finished {player}.", "{player} lost to a pile of bones with ambition." }
			},
			{
				"leech",
				new string[4] { "{killer} drained {player} in the swamp water.", "{player} was emptied by {killer}, quietly, from below.", "{killer} attached to {player}, and that was that.", "{player} died to a worm with a drinking problem." }
			}
		};

		public static int CategoryCount => Catalog.Count;

		public static string Pick(HitType hitType, string player, string killer, string killerKey = null)
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Expected I4, but got Unknown
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			if (!Catalog.TryGetValue(hitType, out var value) || value.Length == 0)
			{
				value = Fallback;
			}
			string text = "hit:" + (int)hitType;
			string[] lines = value;
			KeyValuePair<string, string[]> keyValuePair = SignatureFor(hitType, killerKey);
			if (keyValuePair.Value != null)
			{
				List<string> list = new List<string>(value.Length + keyValuePair.Value.Length);
				list.AddRange(value);
				list.AddRange(keyValuePair.Value);
				lines = list.ToArray();
				text = text + ":" + keyValuePair.Key;
			}
			string text2 = Phrases.Pick(text, lines).Replace("{player}", player);
			if (text2.Contains("{killer}"))
			{
				string newValue = (string.IsNullOrEmpty(killer) ? "something unseen" : killer);
				text2 = text2.Replace("{killer}", newValue);
			}
			return text2;
		}

		private static KeyValuePair<string, string[]> SignatureFor(HitType hitType, string killerKey)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Invalid comparison between Unknown and I4
			if ((int)hitType != 1 || string.IsNullOrEmpty(killerKey))
			{
				return new KeyValuePair<string, string[]>(null, null);
			}
			string text = killerKey.ToLowerInvariant();
			foreach (KeyValuePair<string, string[]> item in Signature)
			{
				if (text.Contains(item.Key))
				{
					return item;
				}
			}
			return new KeyValuePair<string, string[]>(null, null);
		}

		public static List<string> AllLines()
		{
			List<string> list = new List<string>(Fallback);
			foreach (string[] value in Catalog.Values)
			{
				list.AddRange(value);
			}
			foreach (string[] value2 in Signature.Values)
			{
				list.AddRange(value2);
			}
			return list;
		}
	}
	internal enum AlertLevel
	{
		Info,
		Warning,
		Critical,
		Recovery
	}
	internal static class DiscordNotifier
	{
		private const int MaxQueued = 50;

		private const int PaceMs = 2000;

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

		private static readonly object Gate = new object();

		private static bool _draining;

		private static bool _warnedAboutFailure;

		public static bool Configured
		{
			get
			{
				string value = SkaldConfig.DiscordWebhookUrl.Value;
				if (!string.IsNullOrEmpty(value))
				{
					return value.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
				}
				return false;
			}
		}

		public static void Alert(AlertLevel level, string title, string message)
		{
			if (Configured)
			{
				Enqueue(BuildPayload(level, title, message));
			}
		}

		public static void Achievement(string playerName, Achievement achievement)
		{
			if (Configured && SkaldConfig.DiscordAnnounceAchievements.Value)
			{
				string title = (string.IsNullOrEmpty(playerName) ? achievement.Title : (playerName + " earned \"" + achievement.Title + "\""));
				Enqueue(BuildPayload(AlertLevel.Info, title, achievement.Description));
			}
		}

		private static void Enqueue(string json)
		{
			lock (Gate)
			{
				if (Pending.Count >= 50)
				{
					return;
				}
				Pending.Enqueue(json);
				if (_draining)
				{
					return;
				}
				_draining = true;
			}
			ThreadPool.QueueUserWorkItem(Drain);
		}

		private static void Drain(object _)
		{
			while (true)
			{
				string json;
				lock (Gate)
				{
					if (Pending.Count == 0)
					{
						_draining = false;
						break;
					}
					json = Pending.Dequeue();
				}
				Post(json);
				Thread.Sleep(2000);
			}
		}

		private static void Post(string json)
		{
			string value = SkaldConfig.DiscordWebhookUrl.Value;
			if (string.IsNullOrEmpty(value))
			{
				return;
			}
			try
			{
				try
				{
					ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
				}
				catch (NotSupportedException)
				{
				}
				HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(value);
				httpWebRequest.Method = "POST";
				httpWebRequest.ContentType = "application/json";
				httpWebRequest.UserAgent = "Skald/0.4.0";
				httpWebRequest.Timeout = 10000;
				byte[] bytes = Encoding.UTF8.GetBytes(json);
				httpWebRequest.ContentLength = bytes.Length;
				using (Stream stream = httpWebRequest.GetRequestStream())
				{
					stream.Write(bytes, 0, bytes.Length);
				}
				using (HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse())
				{
					int statusCode = (int)httpWebResponse.StatusCode;
					if (statusCode >= 300)
					{
						SkaldPlugin.Log.LogWarning((object)("Discord webhook returned HTTP " + statusCode + "."));
					}
				}
				_warnedAboutFailure = false;
			}
			catch (WebException ex2)
			{
				if (_warnedAboutFailure)
				{
					return;
				}
				_warnedAboutFailure = true;
				string text = ex2.Message;
				if (ex2.Response is HttpWebResponse httpWebResponse2)
				{
					text = "HTTP " + (int)httpWebResponse2.StatusCode;
					try
					{
						using StreamReader streamReader = new StreamReader(httpWebResponse2.GetResponseStream());
						text = text + " " + streamReader.ReadToEnd();
					}
					catch (Exception)
					{
					}
				}
				SkaldPlugin.Log.LogWarning((object)("Discord webhook post failed (" + Redact(text) + "). Check the URL in the config; further failures will stay quiet until one succeeds."));
			}
			catch (Exception ex4)
			{
				if (!_warnedAboutFailure)
				{
					_warnedAboutFailure = true;
					SkaldPlugin.Log.LogWarning((object)("Discord webhook post failed: " + Redact(ex4.Message)));
				}
			}
		}

		public static int Http(string method, string url, string json, int timeoutMs, out string body)
		{
			body = "";
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			}
			catch (NotSupportedException)
			{
			}
			HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
			httpWebRequest.Method = method;
			httpWebRequest.ContentType = "application/json";
			httpWebRequest.UserAgent = "Skald/0.4.0";
			httpWebRequest.Timeout = timeoutMs;
			httpWebRequest.ReadWriteTimeout = timeoutMs;
			byte[] bytes = Encoding.UTF8.GetBytes(json);
			httpWebRequest.ContentLength = bytes.Length;
			using (Stream stream = httpWebRequest.GetRequestStream())
			{
				stream.Write(bytes, 0, bytes.Length);
			}
			HttpWebResponse httpWebResponse;
			try
			{
				httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
			}
			catch (WebException ex2)
			{
				httpWebResponse = ex2.Response as HttpWebResponse;
				if (httpWebResponse == null)
				{
					throw;
				}
			}
			using (httpWebResponse)
			{
				try
				{
					using StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream());
					body = streamReader.ReadToEnd();
				}
				catch (Exception)
				{
				}
				return (int)httpWebResponse.StatusCode;
			}
		}

		public static string Redact(string message)
		{
			if (string.IsNullOrEmpty(message))
			{
				return message;
			}
			string[] array = new string[2]
			{
				SkaldConfig.DiscordWebhookUrl.Value,
				SkaldConfig.StatusWebhookUrl.Value
			};
			foreach (string text in array)
			{
				if (!string.IsNullOrEmpty(text) && text.Length > 12)
				{
					message = message.Replace(text, "<your webhook>");
				}
			}
			return message;
		}

		private static string BuildPayload(AlertLevel level, string title, string message)
		{
			int value = level switch
			{
				AlertLevel.Critical => 13715803, 
				AlertLevel.Warning => 14722136, 
				AlertLevel.Recovery => 5999706, 
				_ => 5930394, 
			};
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("{\"embeds\":[{");
			stringBuilder.Append("\"title\":\"").Append(Escape(title)).Append("\",");
			stringBuilder.Append("\"description\":\"").Append(Escape(message)).Append("\",");
			stringBuilder.Append("\"color\":").Append(value);
			stringBuilder.Append("}],\"allowed_mentions\":{\"parse\":[]}}");
			return stringBuilder.ToString();
		}

		private static string Escape(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length + 16);
			foreach (char c in s)
			{
				switch (c)
				{
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\r':
					stringBuilder.Append("\\r");
					continue;
				case '\t':
					stringBuilder.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}
	}
	internal struct ForecastResult
	{
		public bool Enough;

		public double Latest;

		public double SlopePerDay;

		public double SpanDays;

		public double DaysUntil;
	}
	internal static class Forecast
	{
		public const int MinPoints = 12;

		public const double MinSpanDays = 2.0;

		public static ForecastResult Linear(IList<KeyValuePair<long, double>> points, double threshold)
		{
			ForecastResult result = new ForecastResult
			{
				DaysUntil = double.PositiveInfinity
			};
			if (points == null || points.Count == 0)
			{
				return result;
			}
			long key = points[0].Key;
			long key2 = points[points.Count - 1].Key;
			result.Latest = points[points.Count - 1].Value;
			result.SpanDays = (double)(key2 - key) / 86400.0;
			if (points.Count < 12 || result.SpanDays < 2.0)
			{
				return result;
			}
			double num = points.Count;
			double num2 = 0.0;
			double num3 = 0.0;
			double num4 = 0.0;
			double num5 = 0.0;
			foreach (KeyValuePair<long, double> point in points)
			{
				double num6 = (double)(point.Key - key) / 86400.0;
				double value = point.Value;
				num2 += num6;
				num3 += value;
				num4 += num6 * num6;
				num5 += num6 * value;
			}
			double num7 = num * num4 - num2 * num2;
			if (Math.Abs(num7) < 1E-09)
			{
				return result;
			}
			result.Enough = true;
			result.SlopePerDay = (num * num5 - num2 * num3) / num7;
			if (result.Latest >= threshold)
			{
				result.DaysUntil = 0.0;
			}
			else if (result.SlopePerDay > 1E-06)
			{
				result.DaysUntil = (threshold - result.Latest) / result.SlopePerDay;
			}
			return result;
		}

		public static string Growth(ForecastResult f)
		{
			if (!f.Enough)
			{
				return "not enough history yet (needs about 2 days of samples)";
			}
			if (f.SlopePerDay < 1.0)
			{
				return "holding steady";
			}
			return "about " + Math.Round(f.SlopePerDay).ToString("N0", CultureInfo.InvariantCulture) + " new objects a day";
		}

		public static string Describe(ForecastResult f)
		{
			if (!f.Enough)
			{
				return "not enough history yet (needs about 2 days of samples)";
			}
			if (f.DaysUntil <= 0.0)
			{
				return "already past it";
			}
			if (double.IsInfinity(f.DaysUntil))
			{
				return "not rising, so no limit in sight";
			}
			if (f.DaysUntil < 14.0)
			{
				return "about " + Phrases.Plural((int)Math.Round(f.DaysUntil), "day");
			}
			if (f.DaysUntil < 182.0)
			{
				return "about " + Phrases.Plural((int)Math.Round(f.DaysUntil / 7.0), "week");
			}
			return "more than 6 months";
		}
	}
	[HarmonyPatch(typeof(ZoneSystem), "GlobalKeyAdd")]
	internal static class GlobalKeyAddPatch
	{
		private static void Postfix(string keyStr)
		{
			if (SkaldRuntime.IsServer)
			{
				ServerHub.OnGlobalKey(keyStr);
			}
		}
	}
	internal sealed class HealthSample
	{
		public long Time;

		public int Zdos;

		public long ProcessMb;

		public float FrameMs;

		public int Peers;
	}
	internal sealed class CensusLine
	{
		public long Time;

		public string Category;

		public int Count;
	}
	internal sealed class HealthSummary
	{
		public long PeakMb;

		public float WorstFrameMs;

		public double SlowestSaveSeconds;

		public int ZdosNow;

		public int ZdosAtStart;

		public bool HaveZdoHistory;

		public ForecastResult ZdoForecast;
	}
	internal static class HealthLog
	{
		public const double WriteIntervalSeconds = 900.0;

		private const int KeepDays = 35;

		private static readonly List<HealthSample> Samples = new List<HealthSample>();

		private static readonly List<KeyValuePair<long, double>> Saves = new List<KeyValuePair<long, double>>();

		private static readonly List<CensusLine> Census = new List<CensusLine>();

		private static string _path;

		private static double _sinceWrite;

		private static long _peakMb;

		private static float _worstFrame;

		public static bool Ready => _path != null;

		public static void TryInit()
		{
			if (_path != null || !SkaldStore.Loaded)
			{
				return;
			}
			_path = SkaldStore.SiblingPath(".skald-health.tsv");
			if (_path == null)
			{
				return;
			}
			Samples.Clear();
			Saves.Clear();
			Census.Clear();
			_sinceWrite = 0.0;
			try
			{
				if (File.Exists(_path))
				{
					Load(File.ReadAllLines(_path));
				}
			}
			catch (Exception ex)
			{
				SkaldPlugin.Log.LogWarning((object)("Could not read health log (" + ex.Message + "); starting a new one."));
			}
		}

		public static void Reset()
		{
			_path = null;
			Samples.Clear();
			Saves.Clear();
			Census.Clear();
		}

		public static void RecordCensus(CensusSnapshot snapshot)
		{
			TryInit();
			if (_path == null || snapshot == null)
			{
				return;
			}
			StringBuilder stringBuilder = new StringBuilder();
			Add(snapshot.Time, "_total", snapshot.Total, stringBuilder);
			foreach (KeyValuePair<string, int> category in snapshot.Categories)
			{
				Add(snapshot.Time, category.Key, category.Value, stringBuilder);
			}
			try
			{
				File.AppendAllText(_path, stringBuilder.ToString());
			}
			catch (Exception ex)
			{
				SkaldPlugin.Log.LogWarning((object)("Could not append the census to the health log: " + ex.Message));
			}
		}

		private static void Add(long time, string category, int count, StringBuilder sb)
		{
			Census.Add(new CensusLine
			{
				Time = time,
				Category = category,
				Count = count
			});
			sb.Append("c\t").Append(time).Append('\t')
				.Append(category.Replace('\t', ' '))
				.Append('\t')
				.Append(count)
				.Append(Environment.NewLine);
		}

		public static Dictionary<string, int> CensusAt(long since)
		{
			long num = long.MaxValue;
			foreach (CensusLine censu in Census)
			{
				if (censu.Time >= since && censu.Time < num)
				{
					num = censu.Time;
				}
			}
			if (num == long.MaxValue)
			{
				return null;
			}
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			foreach (CensusLine censu2 in Census)
			{
				if (censu2.Time == num)
				{
					dictionary[censu2.Category] = censu2.Count;
				}
			}
			return dictionary;
		}

		public static void Record(double elapsedSeconds, int zdos, long processMb, float frameMs, int peers)
		{
			if (processMb > _peakMb)
			{
				_peakMb = processMb;
			}
			if (frameMs > _worstFrame)
			{
				_worstFrame = frameMs;
			}
			_sinceWrite += elapsedSeconds;
			if (!(_sinceWrite < 900.0))
			{
				TryInit();
				if (_path != null)
				{
					HealthSample healthSample = new HealthSample
					{
						Time = Now(),
						Zdos = zdos,
						ProcessMb = _peakMb,
						FrameMs = _worstFrame,
						Peers = peers
					};
					Samples.Add(healthSample);
					Append("h\t" + healthSample.Time + "\t" + zdos + "\t" + _peakMb + "\t" + _worstFrame.ToString("0.0", CultureInfo.InvariantCulture) + "\t" + peers);
					_sinceWrite = 0.0;
					_peakMb = 0L;
					_worstFrame = 0f;
				}
			}
		}

		public static void RecordSave(double seconds)
		{
			TryInit();
			if (_path != null)
			{
				long key = Now();
				Saves.Add(new KeyValuePair<long, double>(key, seconds));
				Append("s\t" + key + "\t" + seconds.ToString("0.00", CultureInfo.InvariantCulture));
			}
		}

		public static HealthSummary Summarize(long sinceUnix, int zdoThreshold, int zdosNow, long currentMb)
		{
			HealthSummary healthSummary = new HealthSummary
			{
				ZdosNow = zdosNow,
				PeakMb = currentMb
			};
			foreach (HealthSample sample in Samples)
			{
				if (sample.Time >= sinceUnix)
				{
					if (sample.ProcessMb > healthSummary.PeakMb)
					{
						healthSummary.PeakMb = sample.ProcessMb;
					}
					if (sample.FrameMs > healthSummary.WorstFrameMs)
					{
						healthSummary.WorstFrameM