Decompiled source of TheRavensCall v1.7.2

plugins/TheRavensCall.dll

Decompiled 17 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace TheRavensCall
{
	public class Companion : MonoBehaviour
	{
		public const string NAME = "TheRavensCall";

		public const string VERSION = "1.7.2";

		internal static Companion Instance;

		internal static ManualLogSource Log;

		internal static bool _recipesPushed;

		private static bool _warnedStaleDiskDashboard;

		internal static volatile string _stateCache;

		internal const string EmptyStateJson = "{\"generated_at\":\"\",\"world_name\":\"\",\"day\":0,\"online_count\":0,\"raid_active\":false,\"raid_type\":\"\",\"players\":{}}";

		internal static volatile string _activityCache;

		internal const string EmptyActivityJson = "{\"generated_at\":\"\",\"season\":{\"active\":false,\"name\":\"\",\"started_at\":null,\"standings_since\":null,\"last_ended\":null,\"last_ended_at\":null,\"standings\":[]},\"events\":[]}";

		internal static volatile string _censusCache;

		private HttpListener _listener;

		private Thread _serverThread;

		internal static string OutputDir;

		private void Awake()
		{
			Instance = this;
			Log = Plugin.Log;
			OutputDir = Path.Combine(Paths.ConfigPath, "TheRavensCall");
			Directory.CreateDirectory(OutputDir);
			MethodInfo method = typeof(Player).GetMethod("OnDeath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			Log.LogInfo((object)("Player.OnDeath method found: " + ((method != null) ? method.Name : "NOT FOUND")));
			Log.LogInfo((object)"TheRavensCall v1.7.2 loaded.");
			Log.LogInfo((object)("Writing data to: " + OutputDir));
			if (Plugin.EnableHttpServer.Value)
			{
				StartHttpServer();
			}
		}

		private void Update()
		{
			if (!_recipesPushed && (Object)(object)ObjectDB.instance != (Object)null && ObjectDB.instance.m_recipes.Count > 0)
			{
				_recipesPushed = true;
				((MonoBehaviour)this).StartCoroutine(WriteGameData());
			}
		}

		private void OnDestroy()
		{
			StopHttpServer();
		}

		private void StartHttpServer()
		{
			try
			{
				int value = Plugin.HttpServerPort.Value;
				_listener = new HttpListener();
				_listener.Prefixes.Add("http://localhost:" + value + "/");
				bool flag = Plugin.HttpBindAllInterfaces != null && Plugin.HttpBindAllInterfaces.Value;
				if (flag)
				{
					_listener.Prefixes.Add("http://+:" + value + "/");
				}
				_listener.Start();
				_serverThread = new Thread(HandleRequests)
				{
					IsBackground = true,
					Name = "SteveHTTP"
				};
				_serverThread.Start();
				Log.LogInfo((object)("HTTP server listening on http://localhost:" + value + (flag ? " and every interface" : " (localhost only; set HttpBindAllInterfaces=true to expose it)")));
				if (flag)
				{
					Log.LogInfo((object)("Mobile devices: http://" + GetLocalIP() + ":" + value));
				}
				if (!string.IsNullOrEmpty(Plugin.HttpApiToken?.Value))
				{
					Log.LogInfo((object)"HTTP API token required on /api/state, /api/gamedata, /api/pins, /api/activity and /api/census.");
				}
			}
			catch (Exception ex)
			{
				Log.LogError((object)("HTTP server failed to start: " + ex.Message));
			}
		}

		private static string GetLocalIP()
		{
			try
			{
				using UdpClient udpClient = new UdpClient("8.8.8.8", 80);
				return ((IPEndPoint)udpClient.Client.LocalEndPoint).Address.ToString();
			}
			catch
			{
				return "localhost";
			}
		}

		private void StopHttpServer()
		{
			try
			{
				_listener?.Stop();
			}
			catch
			{
			}
			try
			{
				_serverThread?.Abort();
			}
			catch
			{
			}
		}

		private void HandleRequests()
		{
			while (_listener != null && _listener.IsListening)
			{
				try
				{
					HttpListenerContext ctx = _listener.GetContext();
					ThreadPool.QueueUserWorkItem(delegate
					{
						ProcessRequest(ctx);
					});
				}
				catch
				{
				}
			}
		}

		private void ProcessRequest(HttpListenerContext ctx)
		{
			try
			{
				string text = ctx.Request.Url.AbsolutePath.TrimEnd(new char[1] { '/' });
				if (string.IsNullOrEmpty(text))
				{
					text = "/";
				}
				ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
				ctx.Response.AddHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
				ctx.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, X-Api-Token");
				if (ctx.Request.HttpMethod == "OPTIONS")
				{
					ctx.Response.StatusCode = 204;
					ctx.Response.Close();
					return;
				}
				if (text == "/favicon.ico")
				{
					ctx.Response.StatusCode = 204;
					ctx.Response.Close();
					return;
				}
				if (!ApiTokenOk(ctx, text))
				{
					byte[] bytes = Encoding.UTF8.GetBytes("{\"error\":\"token required\"}");
					ctx.Response.StatusCode = 401;
					ctx.Response.ContentType = "application/json";
					ctx.Response.ContentLength64 = bytes.Length;
					ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
					ctx.Response.Close();
					return;
				}
				byte[] bytes2 = Encoding.UTF8.GetBytes("Not found");
				switch (text)
				{
				case "/api/state":
					ctx.Response.ContentType = "application/json";
					bytes2 = Encoding.UTF8.GetBytes(_stateCache ?? "{\"generated_at\":\"\",\"world_name\":\"\",\"day\":0,\"online_count\":0,\"raid_active\":false,\"raid_type\":\"\",\"players\":{}}");
					break;
				case "/api/gamedata":
				{
					ctx.Response.ContentType = "application/json";
					string path = Path.Combine(OutputDir, "BarrkBOT_data2.json");
					string s = (File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : "{\"recipes\":[],\"items\":[],\"buildables\":[]}");
					bytes2 = Encoding.UTF8.GetBytes(s);
					break;
				}
				case "/api/health":
					ctx.Response.ContentType = "application/json";
					bytes2 = Encoding.UTF8.GetBytes("{\"status\":\"ok\",\"version\":\"1.7.2\"}");
					break;
				case "/api/pins":
					ctx.Response.ContentType = "application/json";
					bytes2 = Encoding.UTF8.GetBytes(MapPinTracker.GetJson());
					break;
				case "/api/activity":
					ctx.Response.ContentType = "application/json";
					bytes2 = Encoding.UTF8.GetBytes(_activityCache ?? "{\"generated_at\":\"\",\"season\":{\"active\":false,\"name\":\"\",\"started_at\":null,\"standings_since\":null,\"last_ended\":null,\"last_ended_at\":null,\"standings\":[]},\"events\":[]}");
					break;
				case "/api/census":
					ctx.Response.ContentType = "application/json";
					bytes2 = Encoding.UTF8.GetBytes(_censusCache ?? WorldCensus.EmptyEnvelope());
					break;
				default:
					if (!text.EndsWith(".html"))
					{
						ctx.Response.StatusCode = 404;
						bytes2 = Encoding.UTF8.GetBytes("Not found");
						break;
					}
					goto case "/";
				case "/":
				case "/index":
				{
					string text2 = Path.Combine(OutputDir, "theravenscall.html");
					if (!File.Exists(text2))
					{
						text2 = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "theravenscall.html");
					}
					if (File.Exists(text2))
					{
						ctx.Response.ContentType = "text/html; charset=utf-8";
						bytes2 = File.ReadAllBytes(text2);
						if (!_warnedStaleDiskDashboard && !ContainsApiMarker(bytes2))
						{
							_warnedStaleDiskDashboard = true;
							Log.LogWarning((object)("theravenscall.html on disk (" + text2 + ") predates 1.3.0 and cannot read the new /api/state shape. Delete it so the 1.3.0 page embedded in this DLL is served instead."));
						}
						break;
					}
					byte[] array = ReadEmbeddedHtml();
					if (array != null)
					{
						ctx.Response.ContentType = "text/html; charset=utf-8";
						bytes2 = array;
					}
					else
					{
						ctx.Response.StatusCode = 404;
						bytes2 = Encoding.UTF8.GetBytes("<h2>theravenscall.html not found.<br>Place it in: " + OutputDir + "</h2>");
						ctx.Response.ContentType = "text/html";
					}
					break;
				}
				}
				ctx.Response.ContentLength64 = bytes2.Length;
				ctx.Response.OutputStream.Write(bytes2, 0, bytes2.Length);
				ctx.Response.Close();
			}
			catch (Exception ex)
			{
				Log.LogError((object)("HTTP request error: " + ex.Message));
				try
				{
					ctx.Response.StatusCode = 500;
					ctx.Response.Close();
				}
				catch
				{
				}
			}
		}

		private static bool ContainsApiMarker(byte[] htmlBytes)
		{
			try
			{
				return Encoding.UTF8.GetString(htmlBytes).IndexOf("theravenscall-api", StringComparison.OrdinalIgnoreCase) >= 0;
			}
			catch
			{
				return true;
			}
		}

		private static byte[] ReadEmbeddedHtml()
		{
			try
			{
				using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("theravenscall.html");
				if (stream == null)
				{
					return null;
				}
				using MemoryStream memoryStream = new MemoryStream();
				stream.CopyTo(memoryStream);
				return memoryStream.ToArray();
			}
			catch (Exception ex)
			{
				Log.LogError((object)("Embedded theravenscall.html read failed: " + ex.Message));
				return null;
			}
		}

		internal static void PrimeStateCache()
		{
			if ((Object)(object)Instance == (Object)null || Plugin.EnableHttpServer == null || (!Plugin.EnableHttpServer.Value && !PushClient.Enabled))
			{
				return;
			}
			try
			{
				string worldName = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetWorldName() : "Unknown");
				int day = (((Object)(object)EnvMan.instance != (Object)null) ? EnvMan.instance.GetDay() : 0);
				int onlineCount = Plugin.GetConnectedPlayers().Count();
				_stateCache = PlayerRegistry.BuildBarrkBotJson(worldName, day, onlineCount);
			}
			catch (Exception ex)
			{
				Log.LogError((object)("state cache prime: " + ex.Message));
			}
		}

		internal static void BuildActivityCache()
		{
			if ((Object)(object)Instance == (Object)null || Plugin.EnableHttpServer == null || (!Plugin.EnableHttpServer.Value && !PushClient.Enabled))
			{
				return;
			}
			try
			{
				string text = DateTime.UtcNow.ToString("o");
				_activityCache = "{\"generated_at\":\"" + text + "\"," + SeasonSystem.SeasonJson() + ",\"events\":" + EventFeed.ToJsonArray() + "}";
			}
			catch (Exception ex)
			{
				Log.LogError((object)("activity cache build: " + ex.Message));
			}
		}

		private static bool ApiTokenOk(HttpListenerContext ctx, string path)
		{
			string text = Plugin.HttpApiToken?.Value;
			if (string.IsNullOrEmpty(text))
			{
				return true;
			}
			if (path != "/api/state" && path != "/api/gamedata" && path != "/api/pins" && path != "/api/activity" && path != "/api/census")
			{
				return true;
			}
			string text2 = ctx.Request.QueryString["token"] ?? ctx.Request.Headers["X-Api-Token"];
			if (!string.IsNullOrEmpty(text2))
			{
				return string.Equals(text2, text, StringComparison.Ordinal);
			}
			return false;
		}

		internal static string ExtractJsonField(string json, string key, bool isComplex)
		{
			if (json == null)
			{
				return null;
			}
			string text = "\"" + key + "\":";
			int num = json.IndexOf(text, StringComparison.Ordinal);
			if (num < 0)
			{
				return null;
			}
			for (num += text.Length; num < json.Length && json[num] == ' '; num++)
			{
			}
			if (num >= json.Length)
			{
				return null;
			}
			if (!isComplex)
			{
				if (json[num] == '"')
				{
					num++;
					StringBuilder stringBuilder = new StringBuilder();
					bool flag = false;
					while (num < json.Length)
					{
						char c = json[num++];
						if (flag)
						{
							stringBuilder.Append(c);
							flag = false;
							continue;
						}
						switch (c)
						{
						case '\\':
							flag = true;
							continue;
						default:
							stringBuilder.Append(c);
							continue;
						case '"':
							break;
						}
						break;
					}
					return stringBuilder.ToString();
				}
				int num2 = num;
				for (; num < json.Length && json[num] != ',' && json[num] != '}' && json[num] != ']'; num++)
				{
				}
				return json.Substring(num2, num - num2).Trim();
			}
			char c2 = json[num];
			char c3 = ((c2 == '{') ? '}' : ']');
			int num3 = 0;
			int num4 = num;
			bool flag2 = false;
			bool flag3 = false;
			while (num < json.Length)
			{
				char c4 = json[num];
				if (flag3)
				{
					flag3 = false;
					num++;
					continue;
				}
				if (c4 == '\\' && flag2)
				{
					flag3 = true;
					num++;
					continue;
				}
				if (c4 == '"')
				{
					flag2 = !flag2;
					num++;
					continue;
				}
				if (flag2)
				{
					num++;
					continue;
				}
				if (c4 == c2)
				{
					num3++;
				}
				else if (c4 == c3)
				{
					num3--;
					if (num3 == 0)
					{
						num++;
						break;
					}
				}
				num++;
			}
			return json.Substring(num4, num - num4);
		}

		public static string B(bool v)
		{
			if (!v)
			{
				return "false";
			}
			return "true";
		}

		public static string Esc(string s)
		{
			if (s == null)
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder();
			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.Append(c);
				}
			}
			return stringBuilder.ToString();
		}

		public static string F(float v)
		{
			if (!float.IsNaN(v) && !float.IsInfinity(v))
			{
				return v.ToString("F1", CultureInfo.InvariantCulture);
			}
			return "0.0";
		}

		public static string Loc(string key)
		{
			if (string.IsNullOrEmpty(key))
			{
				return "";
			}
			try
			{
				Localization instance = Localization.instance;
				if (instance != null)
				{
					return instance.Localize(key) ?? key;
				}
			}
			catch
			{
			}
			return key;
		}

		internal static Biome GetBiome(Player player)
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null)
			{
				return (Biome)0;
			}
			return GetBiomeAt(((Component)player).transform.position);
		}

		internal static Biome GetBiomeAt(Vector3 position)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			if (WorldGenerator.instance == null)
			{
				return (Biome)0;
			}
			return WorldGenerator.instance.GetBiome(position);
		}

		internal static void PollAllPlayers()
		{
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				List<(string, Vector3, long, long)> list = Plugin.GetConnectedPlayers().ToList();
				foreach (var item in list)
				{
					if (!string.IsNullOrEmpty(item.Item1))
					{
						PlayerRecord playerRecord = PlayerRegistry.Get(item.Item1);
						playerRecord.Online = true;
						playerRecord.LastSeen = DateTime.UtcNow.ToString("o");
						BiomeAndGearTracking.CheckBiome(item.Item1, item.Item2, playerRecord);
					}
				}
				PlayerRegistry.SaveDirty();
				string worldName = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetWorldName() : "Unknown");
				int day = (((Object)(object)EnvMan.instance != (Object)null) ? EnvMan.instance.GetDay() : 0);
				string text = PlayerRegistry.BuildBarrkBotJson(worldName, day, list.Count);
				PlayerRegistry.AtomicWrite(Path.Combine(OutputDir, "BarrkBOT_data1.json"), text);
				EventFeed.Save();
				if ((Object)(object)Instance != (Object)null && (Plugin.EnableHttpServer.Value || PushClient.Enabled))
				{
					_stateCache = text;
					BuildActivityCache();
					WorldCensus.MaybeRun();
					PushClient.MaybeSend();
				}
				Log.LogInfo((object)$"Poll tick — {list.Count} online, {PlayerRegistry.All.Count()} known player(s).");
			}
			catch (Exception ex)
			{
				Log.LogError((object)("PollAllPlayers: " + ex));
			}
		}

		internal IEnumerator WriteGameData()
		{
			Log.LogInfo((object)"Writing game data...");
			string text;
			try
			{
				text = RecipeDumper.BuildJson();
			}
			catch (Exception ex)
			{
				Log.LogError((object)("RecipeDumper: " + ex));
				yield break;
			}
			StringBuilder stringBuilder = new StringBuilder("[");
			try
			{
				foreach (GameObject item in ObjectDB.instance.m_items)
				{
					if (!((Object)(object)item == (Object)null))
					{
						ItemDrop component = item.GetComponent<ItemDrop>();
						if (!((Object)(object)component == (Object)null))
						{
							SharedData shared = component.m_itemData.m_shared;
							string text2 = Esc(shared.m_name);
							string text3 = Esc(Loc(shared.m_name));
							string text4 = ((object)Unsafe.As<ItemType, ItemType>(ref shared.m_itemType)/*cast due to .constrained prefix*/).ToString();
							stringBuilder.Append("{\"slug\":\"" + text2 + "\",\"name\":\"" + text3 + "\",\"category\":\"" + text4 + "\"},");
						}
					}
				}
				if (stringBuilder.Length > 1)
				{
					stringBuilder.Length--;
				}
			}
			catch (Exception ex2)
			{
				Log.LogError((object)("Items build error: " + ex2));
			}
			stringBuilder.Append("]");
			StringBuilder stringBuilder2 = new StringBuilder("[");
			try
			{
				bool flag = true;
				foreach (GameObject item2 in ObjectDB.instance.m_items)
				{
					if ((Object)(object)item2 == (Object)null)
					{
						continue;
					}
					ItemDrop component2 = item2.GetComponent<ItemDrop>();
					if ((Object)(object)component2 == (Object)null)
					{
						continue;
					}
					SharedData shared2 = component2.m_itemData.m_shared;
					if ((Object)(object)shared2.m_buildPieces == (Object)null)
					{
						continue;
					}
					foreach (GameObject piece in shared2.m_buildPieces.m_pieces)
					{
						if ((Object)(object)piece == (Object)null)
						{
							continue;
						}
						Piece component3 = piece.GetComponent<Piece>();
						if ((Object)(object)component3 == (Object)null)
						{
							continue;
						}
						string text5 = Esc(Loc(component3.m_name));
						string text6 = ((object)Unsafe.As<PieceCategory, PieceCategory>(ref component3.m_category)/*cast due to .constrained prefix*/).ToString();
						StringBuilder stringBuilder3 = new StringBuilder("[");
						bool flag2 = true;
						Requirement[] resources = component3.m_resources;
						foreach (Requirement val in resources)
						{
							if (!((Object)(object)val.m_resItem == (Object)null))
							{
								string text7 = Esc(val.m_resItem.m_itemData.m_shared.m_name);
								string text8 = Esc(Loc(val.m_resItem.m_itemData.m_shared.m_name));
								if (!flag2)
								{
									stringBuilder3.Append(",");
								}
								stringBuilder3.Append("{\"slug\":\"" + text7 + "\",\"name\":\"" + text8 + "\",\"qty\":" + val.m_amount + "}");
								flag2 = false;
							}
						}
						stringBuilder3.Append("]");
						if (!flag)
						{
							stringBuilder2.Append(",");
						}
						stringBuilder2.Append("{\"name\":\"" + text5 + "\",\"category\":\"" + text6 + "\",\"ingredients\":" + stringBuilder3?.ToString() + "}");
						flag = false;
					}
				}
			}
			catch (Exception ex3)
			{
				Log.LogError((object)("Buildables build error: " + ex3));
			}
			stringBuilder2.Append("]");
			string text9 = "{\"items\":" + stringBuilder?.ToString() + ",\"recipes\":" + text + ",\"buildables\":" + stringBuilder2?.ToString() + ",\"updated_at\":\"" + DateTime.UtcNow.ToString("o") + "\"}";
			try
			{
				PlayerRegistry.AtomicWrite(Path.Combine(OutputDir, "BarrkBOT_data2.json"), text9);
				Log.LogInfo((object)"Game data written OK");
			}
			catch (Exception ex4)
			{
				Log.LogError((object)("WriteGameData file error: " + ex4));
			}
		}

		internal static void RecordDeathHistory(PlayerRecord rec, Player player)
		{
			//IL_02cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0102: Unknown result type (might be due to invalid IL or missing references)
			//IL_010b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			//IL_011d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0126: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_03e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_03f8: Unknown result type (might be due to invalid IL or missing references)
			//IL_040f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cd: Unknown result type (might be due to invalid IL or missing references)
			Log.LogInfo((object)("Death recorded for " + player.GetPlayerName()));
			string text = "Unknown";
			try
			{
				FieldInfo fieldInfo = AccessTools.Field(typeof(Character), "m_lastHit");
				if (fieldInfo != null)
				{
					object? value = fieldInfo.GetValue(player);
					HitData val = (HitData)((value is HitData) ? value : null);
					if (val != null)
					{
						DamageTypes damage = val.m_damage;
						ZDOID attacker = val.m_attacker;
						if (attacker != ZDOID.None)
						{
							ZDOMan instance = ZDOMan.instance;
							ZDO val2 = ((instance != null) ? instance.GetZDO(attacker) : null);
							if (val2 != null)
							{
								ZNetScene instance2 = ZNetScene.instance;
								GameObject val3 = ((instance2 != null) ? instance2.GetPrefab(val2.GetPrefab()) : null);
								if ((Object)(object)val3 != (Object)null)
								{
									text = FriendlyCreatureName(((Object)val3).name.Replace("(Clone)", "").Trim());
								}
							}
						}
						if (text == "Unknown")
						{
							float fire = damage.m_fire;
							float frost = damage.m_frost;
							float poison = damage.m_poison;
							float spirit = damage.m_spirit;
							float lightning = damage.m_lightning;
							float blunt = damage.m_blunt;
							float slash = damage.m_slash;
							float pierce = damage.m_pierce;
							float num = fire + frost + poison + spirit + lightning;
							float num2 = blunt + slash + pierce;
							if (num > 0f)
							{
								float num3 = Math.Max(fire, Math.Max(frost, Math.Max(poison, Math.Max(spirit, lightning))));
								text = ((num3 == fire) ? "Fire" : ((num3 == frost) ? "Frost" : ((num3 == poison) ? "Poison" : ((num3 != spirit) ? "Lightning" : "Spirit"))));
							}
							else if (num2 > 0f && val.m_attacker == ZDOID.None)
							{
								bool flag = false;
								try
								{
									flag = ((Character)player).IsSwimming() || ((Character)player).InWater();
								}
								catch
								{
								}
								text = (flag ? "Drowning" : "Fall Damage");
							}
							else if (num2 > 0f)
							{
								text = "Combat";
							}
							else
							{
								bool flag2 = false;
								try
								{
									flag2 = ((Character)player).IsSwimming() || ((Character)player).InWater();
								}
								catch
								{
								}
								text = (flag2 ? "Drowning" : "Fall Damage");
							}
						}
					}
					else
					{
						bool flag3 = false;
						try
						{
							flag3 = ((Character)player).IsSwimming() || ((Character)player).InWater();
						}
						catch
						{
						}
						text = (flag3 ? "Drowning" : "Fall Damage");
					}
				}
				else
				{
					bool flag4 = false;
					try
					{
						flag4 = ((Character)player).IsSwimming() || ((Character)player).InWater();
					}
					catch
					{
					}
					text = (flag4 ? "Drowning" : "Fall Damage");
				}
			}
			catch (Exception ex)
			{
				Log.LogWarning((object)("Death cause lookup: " + ex.Message));
			}
			Vector3 position = ((Component)player).transform.position;
			string s = ((object)GetBiome(player)/*cast due to .constrained prefix*/).ToString();
			StringBuilder stringBuilder = new StringBuilder("[");
			bool flag5 = true;
			foreach (ItemData allItem in ((Humanoid)player).GetInventory().GetAllItems())
			{
				if (!flag5)
				{
					stringBuilder.Append(",");
				}
				string text2 = Esc(Loc(allItem.m_shared.m_name));
				stringBuilder.Append("{\"name\":\"" + text2 + "\",\"qty\":" + allItem.m_stack + "}");
				flag5 = false;
			}
			stringBuilder.Append("]");
			string item = "{\"timestamp\":\"" + DateTime.UtcNow.ToString("o") + "\",\"killer\":\"" + Esc(text) + "\",\"location\":{\"x\":" + F(position.x) + ",\"y\":" + F(position.y) + ",\"z\":" + F(position.z) + "},\"biome\":\"" + Esc(s) + "\",\"items\":" + stringBuilder?.ToString() + "}";
			rec.DeathHistory.Insert(0, item);
			if (rec.DeathHistory.Count > 10)
			{
				rec.DeathHistory.RemoveAt(rec.DeathHistory.Count - 1);
			}
		}

		internal static void RecordDeathHistoryFromReport(PlayerRecord rec, string cause, Vector3 position)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_006a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			string s = ((object)GetBiomeAt(position)/*cast due to .constrained prefix*/).ToString();
			string item = "{\"timestamp\":\"" + DateTime.UtcNow.ToString("o") + "\",\"killer\":\"" + Esc(cause) + "\",\"location\":{\"x\":" + F(position.x) + ",\"y\":" + F(position.y) + ",\"z\":" + F(position.z) + "},\"biome\":\"" + Esc(s) + "\",\"items\":[]}";
			rec.DeathHistory.Insert(0, item);
			if (rec.DeathHistory.Count > 10)
			{
				rec.DeathHistory.RemoveAt(rec.DeathHistory.Count - 1);
			}
		}

		private static string FriendlyCreatureName(string prefab)
		{
			if (!new Dictionary<string, string>
			{
				{ "Greyling", "Greyling" },
				{ "Greydwarf", "Greydwarf" },
				{ "Greydwarf_Elite", "Greydwarf Brute" },
				{ "Greydwarf_Shaman", "Greydwarf Shaman" },
				{ "Troll", "Troll" },
				{ "Ghost", "Ghost" },
				{ "Skeleton", "Skeleton" },
				{ "Skeleton_Poison", "Poison Skeleton" },
				{ "SkeletonNoArcher", "Skeleton" },
				{ "Draugr", "Draugr" },
				{ "Draugr_Elite", "Draugr Elite" },
				{ "Draugr_Ranged", "Draugr Archer" },
				{ "Blob", "Blob" },
				{ "BlobElite", "Oozer" },
				{ "Wraith", "Wraith" },
				{ "Abomination", "Abomination" },
				{ "Surtling", "Surtling" },
				{ "Goblin", "Fuling" },
				{ "GoblinArcher", "Fuling Archer" },
				{ "GoblinBrute", "Fuling Berserker" },
				{ "GoblinShaman", "Fuling Shaman" },
				{ "GoblinKing", "Yagluth" },
				{ "Eikthyr", "Eikthyr" },
				{ "gd_king", "The Elder" },
				{ "Bonemass", "Bonemass" },
				{ "Dragon", "Moder" },
				{ "SeekerQueen", "The Queen" },
				{ "Fader", "Fader" },
				{ "Seeker", "Seeker" },
				{ "SeekerBrute", "Seeker Soldier" },
				{ "Tick", "Tick" },
				{ "Gjall", "Gjall" },
				{ "Charred_Melee", "Charred" },
				{ "Charred_Archer", "Charred Archer" },
				{ "Charred_Twitcher", "Charred Twitcher" },
				{ "Charred_Shaman", "Charred Shaman" },
				{ "FallenValkyrie", "Valkyrie" },
				{ "Lox", "Lox" },
				{ "Wolf", "Wolf" },
				{ "Serpent", "Sea Serpent" },
				{ "Deathsquito", "Deathsquito" },
				{ "StoneGolem", "Stone Golem" },
				{ "Bat", "Bat" },
				{ "Fenring", "Fenring" },
				{ "Hare", "Hare" },
				{ "Asksvin", "Asksvin" }
			}.TryGetValue(prefab, out var value))
			{
				return prefab;
			}
			return value;
		}

		internal static string JsonGetString(string json, string key)
		{
			if (json == null)
			{
				return null;
			}
			string text = "\"" + key + "\":\"";
			int num = json.IndexOf(text, StringComparison.Ordinal);
			if (num < 0)
			{
				return null;
			}
			num += text.Length;
			StringBuilder stringBuilder = new StringBuilder();
			bool flag = false;
			while (num < json.Length)
			{
				char c = json[num++];
				if (flag)
				{
					stringBuilder.Append(c);
					flag = false;
					continue;
				}
				switch (c)
				{
				case '\\':
					flag = true;
					continue;
				default:
					stringBuilder.Append(c);
					continue;
				case '"':
					break;
				}
				break;
			}
			return stringBuilder.ToString();
		}

		internal static int JsonGetInt(string json, string key)
		{
			if (json == null)
			{
				return 0;
			}
			string text = "\"" + key + "\":";
			int num = json.IndexOf(text, StringComparison.Ordinal);
			if (num < 0)
			{
				return 0;
			}
			for (num += text.Length; num < json.Length && json[num] == ' '; num++)
			{
			}
			int num2 = num;
			if (num < json.Length && json[num] == '-')
			{
				num++;
			}
			for (; num < json.Length && char.IsDigit(json[num]); num++)
			{
			}
			int.TryParse(json.Substring(num2, num - num2), out var result);
			return result;
		}

		internal static bool JsonGetBossDefeated(string json, string boss)
		{
			if (json == null)
			{
				return false;
			}
			int num = json.IndexOf("\"bosses\":{", StringComparison.Ordinal);
			if (num < 0)
			{
				return false;
			}
			int num2 = json.IndexOf("}", num + 9);
			if (num2 < 0)
			{
				return false;
			}
			return json.Substring(num + 9, num2 - num - 9).IndexOf("\"" + boss + "\":true", StringComparison.Ordinal) >= 0;
		}

		internal static List<string> JsonGetArray(string json, string key)
		{
			List<string> list = new List<string>();
			if (json == null)
			{
				return list;
			}
			string text = "\"" + key + "\":[";
			int num = json.IndexOf(text, StringComparison.Ordinal);
			if (num < 0)
			{
				return list;
			}
			num += text.Length;
			int num2 = 0;
			int num3 = -1;
			bool flag = false;
			bool flag2 = false;
			while (num < json.Length)
			{
				char c = json[num];
				if (flag2)
				{
					flag2 = false;
					num++;
					continue;
				}
				if (c == '\\' && flag)
				{
					flag2 = true;
					num++;
					continue;
				}
				if (c == '"')
				{
					flag = !flag;
					num++;
					continue;
				}
				if (flag)
				{
					num++;
					continue;
				}
				if (c == '{')
				{
					if (num2 == 0)
					{
						num3 = num;
					}
					num2++;
				}
				else if (c == '}')
				{
					num2--;
					if (num2 == 0 && num3 >= 0)
					{
						list.Add(json.Substring(num3, num - num3 + 1));
						num3 = -1;
					}
				}
				else if (c == ']' && num2 == 0)
				{
					break;
				}
				num++;
			}
			return list;
		}

		internal static List<string> JsonGetStringArray(string json, string key)
		{
			List<string> list = new List<string>();
			if (json == null)
			{
				return list;
			}
			string text = "\"" + key + "\":[";
			int num = json.IndexOf(text, StringComparison.Ordinal);
			if (num < 0)
			{
				return list;
			}
			num += text.Length;
			bool flag = false;
			bool flag2 = false;
			int num2 = -1;
			while (num < json.Length)
			{
				char c = json[num];
				if (flag2)
				{
					flag2 = false;
					num++;
				}
				else if (c == '\\' && flag)
				{
					flag2 = true;
					num++;
				}
				else if (c == '"')
				{
					if (!flag)
					{
						flag = true;
						num2 = num + 1;
					}
					else
					{
						if (num2 >= 0)
						{
							list.Add(json.Substring(num2, num - num2));
						}
						flag = false;
						num2 = -1;
					}
					num++;
				}
				else
				{
					if (!flag && c == ']')
					{
						break;
					}
					num++;
				}
			}
			return list;
		}

		internal static long JsonGetLong(string json, string key)
		{
			if (json == null)
			{
				return 0L;
			}
			string text = "\"" + key + "\":";
			int num = json.IndexOf(text, StringComparison.Ordinal);
			if (num < 0)
			{
				return 0L;
			}
			for (num += text.Length; num < json.Length && json[num] == ' '; num++)
			{
			}
			int num2 = num;
			if (num < json.Length && json[num] == '-')
			{
				num++;
			}
			for (; num < json.Length && char.IsDigit(json[num]); num++)
			{
			}
			long.TryParse(json.Substring(num2, num - num2), out var result);
			return result;
		}

		internal static List<ZDO> GetAllZDOs()
		{
			List<ZDO> list = new List<ZDO>();
			try
			{
				FieldInfo field = typeof(ZDOMan).GetField("m_objectsByID", BindingFlags.Instance | BindingFlags.NonPublic);
				if (field != null && field.GetValue(ZDOMan.instance) is Dictionary<ZDOID, ZDO> dictionary)
				{
					list.AddRange(dictionary.Values);
					return list;
				}
			}
			catch
			{
			}
			return list;
		}
	}
	internal static class RecipeDumper
	{
		public static string BuildJson()
		{
			if ((Object)(object)ObjectDB.instance == (Object)null)
			{
				return "[]";
			}
			StringBuilder stringBuilder = new StringBuilder("[");
			foreach (Recipe recipe in ObjectDB.instance.m_recipes)
			{
				if ((Object)(object)recipe == (Object)null || (Object)(object)recipe.m_item == (Object)null)
				{
					continue;
				}
				SharedData shared = recipe.m_item.m_itemData.m_shared;
				string text = Companion.Esc(Companion.Loc(shared.m_name));
				string text2 = Companion.Esc(shared.m_name);
				string text3 = Companion.Esc(((Object)recipe).name);
				string text4 = ((object)Unsafe.As<ItemType, ItemType>(ref shared.m_itemType)/*cast due to .constrained prefix*/).ToString();
				int amount = recipe.m_amount;
				string text5 = (((Object)(object)recipe.m_craftingStation != (Object)null) ? Companion.Esc(Companion.Loc(recipe.m_craftingStation.m_name)) : "Hand");
				StringBuilder stringBuilder2 = new StringBuilder("[");
				Requirement[] resources = recipe.m_resources;
				foreach (Requirement val in resources)
				{
					if (!((Object)(object)val.m_resItem == (Object)null))
					{
						string text6 = Companion.Esc(val.m_resItem.m_itemData.m_shared.m_name);
						string text7 = Companion.Esc(Companion.Loc(val.m_resItem.m_itemData.m_shared.m_name));
						stringBuilder2.Append("{\"slug\":\"" + text6 + "\",\"name\":\"" + text7 + "\",\"qty\":" + val.m_amount + "},");
					}
				}
				if (stringBuilder2.Length > 1)
				{
					stringBuilder2.Length--;
				}
				stringBuilder2.Append("]");
				stringBuilder.Append("{\"slug\":\"" + text2 + "\",\"recipe_key\":\"" + text3 + "\",\"name\":\"" + text + "\",\"category\":\"" + text4 + "\",\"amount\":" + amount + ",\"station\":\"" + text5 + "\",\"ingredients\":" + stringBuilder2?.ToString() + "},");
			}
			if (stringBuilder.Length > 1)
			{
				stringBuilder.Length--;
			}
			stringBuilder.Append("]");
			return stringBuilder.ToString();
		}
	}
	internal static class BossKeys
	{
		private static readonly Dictionary<string, string> Map = new Dictionary<string, string>
		{
			{ "Eikthyr", "eikthyr" },
			{ "gd_king", "elder" },
			{ "Bonemass", "bonemass" },
			{ "Dragon", "moder" },
			{ "GoblinKing", "yagluth" },
			{ "SeekerQueen", "queen" },
			{ "Fader", "fader" }
		};

		public static string Resolve(string prefab)
		{
			if (!Map.TryGetValue(prefab, out var value))
			{
				return null;
			}
			return value;
		}
	}
	internal static class ChestTracker
	{
		public static string GetJson(long playerID)
		{
			StringBuilder stringBuilder = new StringBuilder("[");
			Container[] array = Object.FindObjectsByType<Container>((FindObjectsSortMode)0);
			foreach (Container val in array)
			{
				if ((Object)(object)val == (Object)null)
				{
					continue;
				}
				Inventory inventory = val.GetInventory();
				if (inventory == null || playerID == 0L)
				{
					continue;
				}
				ZNetView component = ((Component)val).GetComponent<ZNetView>();
				if ((Object)(object)component == (Object)null || !component.IsValid())
				{
					continue;
				}
				long num = component.GetZDO().GetLong(ZDOVars.s_creator, 0L);
				if (num == 0L || num != playerID)
				{
					continue;
				}
				foreach (ItemData allItem in inventory.GetAllItems())
				{
					string text = Companion.Esc(allItem.m_shared.m_name);
					string text2 = Companion.Esc(Companion.Loc(allItem.m_shared.m_name));
					string text3 = ((object)Unsafe.As<ItemType, ItemType>(ref allItem.m_shared.m_itemType)/*cast due to .constrained prefix*/).ToString();
					stringBuilder.Append("{\"slug\":\"" + text + "\",\"name\":\"" + text2 + "\",\"qty\":" + allItem.m_stack + ",\"category\":\"" + text3 + "\"},");
				}
			}
			if (stringBuilder.Length > 1)
			{
				stringBuilder.Length--;
			}
			stringBuilder.Append("]");
			return stringBuilder.ToString();
		}
	}
	internal static class TimerTracker
	{
		private static readonly Dictionary<string, string> SMELTER_NAMES = new Dictionary<string, string>
		{
			{ "smelter", "Smelter" },
			{ "charcoal_kiln", "Kiln" },
			{ "blastfurnace", "Blast Furnace" },
			{ "piece_spinningwheel", "Spinning Wheel" },
			{ "windmill", "Windmill" },
			{ "food_smelter", "Food Smelter" },
			{ "eitrrefinery", "Eitr Refinery" }
		};

		private static readonly Dictionary<string, string> PLANT_NAMES = new Dictionary<string, string>
		{
			{ "sapling_carrot", "Carrot" },
			{ "sapling_turnip", "Turnip" },
			{ "sapling_onion", "Onion" },
			{ "sapling_barley", "Barley" },
			{ "sapling_flax", "Flax" },
			{ "sapling_jotunpuffs", "Jotun Puffs" },
			{ "sapling_magecap", "Magecap" },
			{ "sapling_seedcarrot", "Seed Carrot" },
			{ "sapling_seedturnip", "Seed Turnip" },
			{ "sapling_seedonion", "Seed Onion" },
			{ "ChickenEgg", "Chicken Egg" }
		};

		private const float FERMENTER_DURATION = 2400f;

		private const float BEEHIVE_SEC_PER_UNIT = 1200f;

		private const int BEEHIVE_MAX_HONEY = 4;

		private static readonly Dictionary<string, float> PLANT_GROW_TIMES = new Dictionary<string, float>
		{
			{ "sapling_carrot", 4000f },
			{ "sapling_turnip", 4000f },
			{ "sapling_onion", 4000f },
			{ "sapling_barley", 4000f },
			{ "sapling_flax", 4000f },
			{ "sapling_jotunpuffs", 3000f },
			{ "sapling_magecap", 3000f },
			{ "sapling_seedcarrot", 5000f },
			{ "sapling_seedturnip", 5000f },
			{ "sapling_seedonion", 5000f },
			{ "ChickenEgg", 1800f }
		};

		private static readonly Dictionary<string, float> SMELTER_SPP = new Dictionary<string, float>
		{
			{ "smelter", 15f },
			{ "charcoal_kiln", 15f },
			{ "blastfurnace", 15f },
			{ "piece_spinningwheel", 30f },
			{ "windmill", 10f },
			{ "food_smelter", 15f },
			{ "eitrrefinery", 15f }
		};

		private static readonly Dictionary<string, int> _hashCache = new Dictionary<string, int>();

		private static int PrefabHash(string name)
		{
			if (_hashCache.TryGetValue(name, out var value))
			{
				return value;
			}
			try
			{
				if ((Object)(object)ZNetScene.instance == (Object)null)
				{
					return 0;
				}
				FieldInfo field = typeof(ZNetScene).GetField("m_namedPrefabs", BindingFlags.Instance | BindingFlags.NonPublic);
				if (field != null && field.GetValue(ZNetScene.instance) is Dictionary<int, GameObject> dictionary)
				{
					foreach (KeyValuePair<int, GameObject> item in dictionary)
					{
						if ((Object)(object)item.Value != (Object)null && ((Object)item.Value).name == name)
						{
							_hashCache[name] = item.Key;
							return item.Key;
						}
					}
				}
			}
			catch
			{
			}
			return 0;
		}

		public static string GetJson(long playerID)
		{
			if (ZDOMan.instance == null || (Object)(object)ZNet.instance == (Object)null)
			{
				return "{\"fermenters\":[],\"smelters\":[],\"beehives\":[],\"plants\":[]}";
			}
			double timeSeconds = ZNet.instance.GetTimeSeconds();
			long num = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
			List<ZDO> allZDOs = Companion.GetAllZDOs();
			StringBuilder stringBuilder = new StringBuilder();
			StringBuilder stringBuilder2 = new StringBuilder();
			StringBuilder stringBuilder3 = new StringBuilder();
			StringBuilder stringBuilder4 = new StringBuilder();
			bool flag = true;
			Fermenter[] array = Object.FindObjectsByType<Fermenter>((FindObjectsSortMode)0);
			MethodInfo method = typeof(Fermenter).GetMethod("GetFermentationTime", BindingFlags.Instance | BindingFlags.NonPublic);
			MethodInfo method2 = typeof(Fermenter).GetMethod("GetStatus", BindingFlags.Instance | BindingFlags.NonPublic);
			FieldInfo field = typeof(Fermenter).GetField("m_fermentationDuration", BindingFlags.Instance | BindingFlags.Public);
			Fermenter[] array2 = array;
			foreach (Fermenter val in array2)
			{
				try
				{
					ZNetView component = ((Component)val).GetComponent<ZNetView>();
					if (!((Object)(object)component == (Object)null) && component.IsValid() && (playerID == 0L || component.GetZDO().GetLong(ZDOVars.s_creator, 0L) == playerID))
					{
						double num2 = ((method != null) ? ((double)method.Invoke(val, null)) : 0.0);
						MethodInfo method3 = typeof(Fermenter).GetMethod("GetContent", BindingFlags.Instance | BindingFlags.NonPublic);
						string text = ((method3 != null) ? (((string)method3.Invoke(val, null)) ?? "") : "");
						string text2 = "";
						if (!string.IsNullOrEmpty(text))
						{
							ObjectDB instance = ObjectDB.instance;
							GameObject obj = ((instance != null) ? instance.GetItemPrefab(text) : null);
							text2 = Companion.Esc(Companion.Loc(((obj == null) ? null : obj.GetComponent<ItemDrop>()?.m_itemData.m_shared.m_name) ?? text));
						}
						object obj2 = ((method2 != null) ? method2.Invoke(val, null) : null);
						string text3 = ((obj2 != null) ? obj2.ToString() : "Unknown");
						float num3 = ((field != null) ? ((float)field.GetValue(val)) : 2400f);
						float num4 = (float)Math.Max(0.0, (double)num3 - num2);
						float v = ((num3 > 0f) ? (Mathf.Clamp01((float)num2 / num3) * 100f) : 0f);
						if (!flag)
						{
							stringBuilder.Append(",");
						}
						long num5 = ((num4 > 0f) ? (num + (long)(num4 * 1000f)) : 0);
						stringBuilder.Append("{\"status\":\"" + text3 + "\",\"content\":\"" + text2 + "\",\"duration\":" + Companion.F(num3) + ",\"remaining\":" + Companion.F(num4) + ",\"end_ms\":" + num5 + ",\"percent\":" + Companion.F(v) + "}");
						flag = false;
					}
				}
				catch
				{
				}
			}
			bool flag2 = true;
			foreach (KeyValuePair<string, string> sMELTER_NAME in SMELTER_NAMES)
			{
				int num6 = PrefabHash(sMELTER_NAME.Key);
				float value;
				float num7 = (SMELTER_SPP.TryGetValue(sMELTER_NAME.Key, out value) ? value : 15f);
				foreach (ZDO item in allZDOs)
				{
					if (item.GetPrefab() != num6 || (playerID != 0L && item.GetLong(ZDOVars.s_creator, 0L) != playerID))
					{
						continue;
					}
					try
					{
						int num8 = (int)item.GetFloat(ZDOVars.s_fuel, 0f);
						int num9 = item.GetInt(ZDOVars.s_queued, 0);
						float num10 = item.GetFloat("accTime", 0f);
						float num11 = ((num9 > 0) ? (num7 * (float)num9 - num10) : 0f);
						if (num11 < 0f)
						{
							num11 = 0f;
						}
						string text4 = ((sMELTER_NAME.Key == "blastfurnace") ? item.GetString("SpawnOre", "") : item.GetString(ZDOVars.s_item, ""));
						string text5 = "";
						if (!string.IsNullOrEmpty(text4))
						{
							ObjectDB instance2 = ObjectDB.instance;
							GameObject obj4 = ((instance2 != null) ? instance2.GetItemPrefab(text4) : null);
							text5 = Companion.Esc(Companion.Loc(((obj4 == null) ? null : obj4.GetComponent<ItemDrop>()?.m_itemData.m_shared.m_name) ?? text4));
						}
						if (!flag2)
						{
							stringBuilder2.Append(",");
						}
						long num12 = ((num11 > 0f) ? (num + (long)(num11 * 1000f)) : 0);
						stringBuilder2.Append("{\"name\":\"" + Companion.Esc(sMELTER_NAME.Value) + "\",\"fuel\":" + num8 + ",\"queue\":" + num9 + ",\"input\":\"" + text5 + "\",\"sec_per_item\":" + Companion.F(num7) + ",\"remaining\":" + Companion.F(num11) + ",\"end_ms\":" + num12 + "}");
						flag2 = false;
					}
					catch
					{
					}
				}
			}
			int num13 = PrefabHash("piece_beehive");
			bool flag3 = true;
			if (num13 != 0)
			{
				foreach (ZDO item2 in allZDOs)
				{
					if (item2.GetPrefab() != num13 || (playerID != 0L && item2.GetLong(ZDOVars.s_creator, 0L) != playerID))
					{
						continue;
					}
					try
					{
						int num14 = item2.GetInt(ZDOVars.s_level, 0);
						double num15 = item2.GetFloat(ZDOVars.s_lastTime, 0f);
						double num16 = ((num15 > 0.0) ? (timeSeconds - num15) : 0.0);
						float num17 = (float)Math.Max(0.0, 1200.0 - num16);
						bool flag4 = num14 >= 4;
						string text6 = (flag4 ? "Ready" : ((num14 > 0) ? "Producing" : "Empty"));
						if (!flag3)
						{
							stringBuilder3.Append(",");
						}
						long num18 = ((!flag4 && num17 > 0f) ? (num + (long)(num17 * 1000f)) : 0);
						stringBuilder3.Append("{\"honey\":" + num14 + ",\"max_honey\":" + 4 + ",\"sec_per_unit\":" + Companion.F(1200f) + ",\"remaining\":" + Companion.F(flag4 ? 0f : num17) + ",\"end_ms\":" + num18 + ",\"status\":\"" + text6 + "\"}");
						flag3 = false;
					}
					catch
					{
					}
				}
			}
			bool flag5 = true;
			foreach (KeyValuePair<string, string> pLANT_NAME in PLANT_NAMES)
			{
				int num19 = PrefabHash(pLANT_NAME.Key);
				float value2;
				float num20 = (PLANT_GROW_TIMES.TryGetValue(pLANT_NAME.Key, out value2) ? value2 : 4000f);
				int num21 = 0;
				int num22 = 0;
				float num23 = 0f;
				foreach (ZDO item3 in allZDOs)
				{
					if (item3.GetPrefab() != num19 || (playerID != 0L && item3.GetLong(ZDOVars.s_creator, 0L) != playerID))
					{
						continue;
					}
					try
					{
						double num24 = item3.GetFloat(ZDOVars.s_plantTime, 0f);
						double num25 = ((num24 > 0.0) ? (timeSeconds - num24) : 0.0);
						float num26 = (float)Math.Max(0.0, (double)num20 - num25);
						if (num26 <= 0f)
						{
							num21++;
							continue;
						}
						num22++;
						num23 += num26;
					}
					catch
					{
					}
				}
				if (num21 + num22 != 0)
				{
					float num27 = ((num22 > 0) ? (num23 / (float)num22) : 0f);
					float v2 = ((num20 > 0f && num22 > 0) ? (Mathf.Clamp01(1f - num27 / num20) * 100f) : ((num21 > 0) ? 100f : 0f));
					if (!flag5)
					{
						stringBuilder4.Append(",");
					}
					long num28 = ((num27 > 0f) ? (num + (long)(num27 * 1000f)) : 0);
					stringBuilder4.Append("{\"name\":\"" + Companion.Esc(pLANT_NAME.Value) + "\",\"total\":" + (num21 + num22) + ",\"ready\":" + num21 + ",\"growing\":" + num22 + ",\"avg_remaining\":" + Companion.F(num27) + ",\"end_ms\":" + num28 + ",\"percent\":" + Companion.F(v2) + "}");
					flag5 = false;
				}
			}
			int num29 = PrefabHash("piece_sapcollector");
			StringBuilder stringBuilder5 = new StringBuilder();
			bool flag6 = true;
			if (num29 != 0)
			{
				foreach (ZDO item4 in allZDOs)
				{
					if (item4.GetPrefab() != num29 || (playerID != 0L && item4.GetLong(ZDOVars.s_creator, 0L) != playerID))
					{
						continue;
					}
					try
					{
						bool flag7 = item4.GetBool("wassap", false);
						double num30 = item4.GetFloat("SpawnTime", 0f);
						item4.GetFloat("lastTime", 0f);
						double num31 = ((num30 > 0.0) ? (timeSeconds - num30) : 0.0);
						float num32 = (flag7 ? ((float)Math.Max(0.0, 120.0 - num31)) : 0f);
						string text7 = ((!flag7) ? "Not Connected" : ((num32 <= 0f) ? "Ready" : "Producing"));
						long num33 = ((flag7 && num32 > 0f) ? (num + (long)(num32 * 1000f)) : 0);
						if (!flag6)
						{
							stringBuilder5.Append(",");
						}
						stringBuilder5.Append("{\"connected\":" + Companion.B(flag7) + ",\"remaining\":" + Companion.F(num32) + ",\"end_ms\":" + num33 + ",\"status\":\"" + text7 + "\"}");
						flag6 = false;
					}
					catch
					{
					}
				}
			}
			return "{\"fermenters\":[" + stringBuilder.ToString() + "],\"smelters\":[" + stringBuilder2.ToString() + "],\"beehives\":[" + stringBuilder3.ToString() + "],\"plants\":[" + stringBuilder4.ToString() + "],\"sap_collectors\":[" + stringBuilder5.ToString() + "]}";
		}
	}
	internal static class MapPinTracker
	{
		public static string GetJson()
		{
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_018b: Unknown result type (might be due to invalid IL or missing references)
			//IL_019f: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f3: Unknown result type (might be due to invalid IL or missing references)
			//IL_020c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0225: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				Minimap instance = Minimap.instance;
				if ((Object)(object)instance == (Object)null)
				{
					return "[]";
				}
				FieldInfo field = typeof(Minimap).GetField("m_pins", BindingFlags.Instance | BindingFlags.NonPublic);
				if (field == null)
				{
					return "[]";
				}
				if (!(field.GetValue(instance) is IList { Count: not 0 } list))
				{
					return "[]";
				}
				Type type = list[0].GetType();
				FieldInfo field2 = type.GetField("m_pos");
				FieldInfo field3 = type.GetField("m_type");
				FieldInfo field4 = type.GetField("m_name");
				FieldInfo field5 = type.GetField("m_checked");
				FieldInfo field6 = type.GetField("m_ownerID");
				StringBuilder stringBuilder = new StringBuilder("[");
				bool flag = true;
				foreach (object item in list)
				{
					if (item != null)
					{
						if (!flag)
						{
							stringBuilder.Append(",");
						}
						string text = Companion.Esc((field4 != null) ? (((string)field4.GetValue(item)) ?? "") : "");
						string text2 = ((field3 != null) ? field3.GetValue(item).ToString() : "Unknown");
						bool v = field5 != null && (bool)field5.GetValue(item);
						bool v2 = ((field6 != null) ? ((long)field6.GetValue(item)) : 0) != 0;
						Vector3 val = (Vector3)((field2 != null) ? ((Vector3)field2.GetValue(item)) : Vector3.zero);
						stringBuilder.Append("{\"name\":\"" + text + "\",\"type\":\"" + text2 + "\",\"checked\":" + Companion.B(v) + ",\"shared\":" + Companion.B(v2) + ",\"x\":" + Companion.F(val.x) + ",\"y\":" + Companion.F(val.y) + ",\"z\":" + Companion.F(val.z) + "}");
						flag = false;
					}
				}
				stringBuilder.Append("]");
				return stringBuilder.ToString();
			}
			catch (Exception ex)
			{
				ManualLogSource log = Companion.Log;
				if (log != null)
				{
					log.LogWarning((object)("MapPinTracker: " + ex.Message));
				}
				return "[]";
			}
		}
	}
	internal static class BoatTracker
	{
		public static string GetJson(Player player)
		{
			try
			{
				Ship val = (((Object)(object)player != (Object)null) ? ((Component)player).GetComponentInParent<Ship>() : null);
				if ((Object)(object)val == (Object)null)
				{
					return "[]";
				}
				string text = Companion.Esc(((Object)((Component)val).gameObject).name.Replace("(Clone)", "").Trim().ToLower());
				string text2 = Companion.F(val.GetSpeed());
				WearNTear component = ((Component)val).GetComponent<WearNTear>();
				string text3 = (((Object)(object)component != (Object)null) ? Companion.F(component.GetHealthPercentage() * 1000f) : "0.0");
				return "[{\"name\":\"" + text + "\",\"type\":\"" + text + "\",\"speed\":" + text2 + ",\"health\":" + text3 + ",\"max_health\":1000}]";
			}
			catch
			{
				return "[]";
			}
		}
	}
	public class PlayerRecord
	{
		public string Name;

		public string FirstSeen = "";

		public string LastSeen = "";

		public bool Online;

		public string SessionStart;

		public long PlaytimeSecondsLifetime;

		public int Kills;

		public int Deaths;

		public int GearTier;

		public string ActiveTitle = "";

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

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

		public int BossKillsCredited;

		public Dictionary<string, int> CreatureKills = new Dictionary<string, int>();

		public int SessionKills;

		public float SessionDmgDone;

		public float SessionDmgTaken;

		public int SessionBlocks;

		public int SessionParries;

		public float SessionDmgBlocked;

		public int TotalKillsLifetime;

		public int TotalDeathsLifetime;

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

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

		public List<string> DeathHistory = new List<string>();

		public Dictionary<string, float> VanillaStats = new Dictionary<string, float>();

		public Dictionary<string, float> SkillLevels = new Dictionary<string, float>();

		public Dictionary<string, float> SkillProgress = new Dictionary<string, float>();

		public int BuildsPlaced;

		public int BuildsRemoved;

		public int BuildsRepaired;

		public int ItemsCrafted;

		public int ItemsUpgraded;

		public int ItemsRepaired;

		public Dictionary<string, int> ResourcesHarvested = new Dictionary<string, int>();

		public int ConsumablesEaten;

		public int BossesSummoned;

		public int GuardianPowersUsed;

		public bool Dirty;
	}
	public static class WorldState
	{
		public static bool RaidActive = false;

		public static string RaidType = "";
	}
	public static class PlayerRegistry
	{
		private static readonly Dictionary<string, PlayerRecord> _records = new Dictionary<string, PlayerRecord>(StringComparer.OrdinalIgnoreCase);

		private static readonly HashSet<string> ReservedFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
		{
			"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6",
			"COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7",
			"LPT8", "LPT9"
		};

		private static string PlayersDir => Path.Combine(Paths.ConfigPath, "TheRavensCall", "players");

		public static IEnumerable<PlayerRecord> All => _records.Values;

		public static PlayerRecord Get(string playerName)
		{
			if (string.IsNullOrEmpty(playerName))
			{
				return null;
			}
			if (_records.TryGetValue(playerName, out var value))
			{
				return value;
			}
			value = LoadFromDisk(playerName) ?? new PlayerRecord
			{
				Name = playerName,
				FirstSeen = DateTime.UtcNow.ToString("o")
			};
			value.Name = playerName;
			_records[playerName] = value;
			return value;
		}

		public static void LoadAll()
		{
			try
			{
				Directory.CreateDirectory(PlayersDir);
				string[] files = Directory.GetFiles(PlayersDir, "*.json");
				foreach (string path in files)
				{
					string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
					if (string.IsNullOrEmpty(fileNameWithoutExtension))
					{
						continue;
					}
					string text = null;
					try
					{
						text = Companion.JsonGetString(File.ReadAllText(path, Encoding.UTF8), "name");
					}
					catch
					{
					}
					if (string.IsNullOrEmpty(text))
					{
						text = fileNameWithoutExtension;
					}
					string path2 = SanitizedFileName(text);
					bool flag = !string.Equals(Path.GetFullPath(path), Path.GetFullPath(path2), StringComparison.OrdinalIgnoreCase);
					if (flag && File.Exists(path2))
					{
						continue;
					}
					PlayerRecord playerRecord = LoadFromDisk(text, path);
					if (playerRecord == null)
					{
						continue;
					}
					playerRecord.Name = text;
					_records[text] = playerRecord;
					if (flag)
					{
						try
						{
							playerRecord.Dirty = true;
							Save(playerRecord);
							File.Delete(path);
						}
						catch (Exception ex)
						{
							Plugin.Log.LogWarning((object)("[TheRavensCall] PlayerRegistry: could not migrate " + Path.GetFileName(path) + " to " + Path.GetFileName(path2) + ": " + ex.Message));
						}
					}
				}
				Plugin.Log.LogInfo((object)$"[TheRavensCall] PlayerRegistry loaded {_records.Count} player record(s).");
			}
			catch (Exception ex2)
			{
				Plugin.Log.LogWarning((object)("[TheRavensCall] PlayerRegistry.LoadAll error: " + ex2.Message));
			}
		}

		private static string SanitizedFileName(string playerName)
		{
			return Path.Combine(PlayersDir, SanitizeForFile(playerName) + ".json");
		}

		private static string SanitizeForFile(string name)
		{
			if (string.IsNullOrEmpty(name))
			{
				return "unknown";
			}
			StringBuilder stringBuilder = new StringBuilder();
			char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
			foreach (char c in name)
			{
				stringBuilder.Append((Array.IndexOf(invalidFileNameChars, c) >= 0) ? '_' : c);
			}
			string text = stringBuilder.ToString();
			if (!ReservedFileNames.Contains(text))
			{
				return text;
			}
			return "_" + text;
		}

		internal static void AtomicWrite(string path, string text)
		{
			string text2 = path + ".tmp";
			File.WriteAllText(text2, text, Encoding.UTF8);
			try
			{
				if (File.Exists(path))
				{
					File.Replace(text2, path, null);
				}
				else
				{
					File.Move(text2, path);
				}
			}
			catch (Exception)
			{
				string text3 = path + ".bak";
				if (File.Exists(path))
				{
					try
					{
						File.Delete(text3);
					}
					catch
					{
					}
					File.Move(path, text3);
				}
				try
				{
					File.Move(text2, path);
				}
				catch
				{
					if (File.Exists(text3) && !File.Exists(path))
					{
						File.Move(text3, path);
					}
					throw;
				}
				try
				{
					File.Delete(text3);
				}
				catch
				{
				}
			}
		}

		private static PlayerRecord LoadFromDisk(string playerName, string path = null)
		{
			try
			{
				if (string.IsNullOrEmpty(path))
				{
					path = SanitizedFileName(playerName);
				}
				if (!File.Exists(path))
				{
					return null;
				}
				string text = File.ReadAllText(path, Encoding.UTF8);
				if (!text.TrimEnd(Array.Empty<char>()).EndsWith("}"))
				{
					Plugin.Log.LogWarning((object)("[TheRavensCall] PlayerRegistry: " + Path.GetFileName(path) + " is truncated; " + playerName + " starts from a fresh record. Copy kept as " + Path.GetFileName(path) + ".corrupt"));
					try
					{
						File.Copy(path, path + ".corrupt", overwrite: true);
					}
					catch
					{
					}
					return null;
				}
				return new PlayerRecord
				{
					Name = playerName,
					FirstSeen = (Companion.JsonGetString(text, "first_seen") ?? DateTime.UtcNow.ToString("o")),
					LastSeen = (Companion.JsonGetString(text, "last_seen") ?? ""),
					PlaytimeSecondsLifetime = Companion.JsonGetLong(text, "playtime_seconds_lifetime"),
					Kills = Companion.JsonGetInt(text, "kills_narrative"),
					Deaths = Companion.JsonGetInt(text, "deaths_narrative"),
					GearTier = Companion.JsonGetInt(text, "gear_tier"),
					ActiveTitle = (Companion.JsonGetString(text, "active_title") ?? ""),
					BossKillsCredited = Companion.JsonGetInt(text, "boss_kills_credited"),
					TotalKillsLifetime = Companion.JsonGetInt(text, "kills_observed_lifetime"),
					TotalDeathsLifetime = Companion.JsonGetInt(text, "deaths_lifetime"),
					EarnedTitles = new HashSet<string>(Companion.JsonGetStringArray(text, "titles_earned")),
					BiomesDiscovered = new HashSet<string>(Companion.JsonGetStringArray(text, "biomes_discovered")),
					DefeatedBosses = new HashSet<string>(Companion.JsonGetStringArray(text, "bosses_defeated")),
					CaughtFish = new HashSet<string>(Companion.JsonGetStringArray(text, "caught_fish")),
					DeathHistory = Companion.JsonGetArray(text, "death_history"),
					CreatureKills = ParseCreatureKills(text),
					VanillaStats = ParseStringFloatMap(text, "vanilla_stats"),
					SkillLevels = ParseStringFloatMap(text, "skill_levels"),
					SkillProgress = ParseStringFloatMap(text, "skill_progress"),
					BuildsPlaced = Companion.JsonGetInt(text, "builds_placed"),
					BuildsRemoved = Companion.JsonGetInt(text, "builds_removed"),
					BuildsRepaired = Companion.JsonGetInt(text, "builds_repaired"),
					ItemsCrafted = Companion.JsonGetInt(text, "items_crafted"),
					ItemsUpgraded = Companion.JsonGetInt(text, "items_upgraded"),
					ItemsRepaired = Companion.JsonGetInt(text, "items_repaired"),
					ResourcesHarvested = ParseStringIntMap(text, "resources_harvested"),
					ConsumablesEaten = Companion.JsonGetInt(text, "consumables_eaten"),
					BossesSummoned = Companion.JsonGetInt(text, "bosses_summoned"),
					GuardianPowersUsed = Companion.JsonGetInt(text, "guardian_powers_used")
				};
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[TheRavensCall] PlayerRegistry load failed for " + playerName + ": " + ex.Message));
				return null;
			}
		}

		private static Dictionary<string, int> ParseCreatureKills(string json)
		{
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			string text = Companion.ExtractJsonField(json, "creature_kills", isComplex: true);
			if (string.IsNullOrEmpty(text) || text.Length < 2)
			{
				return dictionary;
			}
			foreach (string item in SplitTopLevel(text.Substring(1, text.Length - 2)))
			{
				int num = item.IndexOf(':');
				if (num >= 0)
				{
					string text2 = item.Substring(0, num).Trim().Trim(new char[1] { '"' });
					string s = item.Substring(num + 1).Trim();
					if (!string.IsNullOrEmpty(text2) && int.TryParse(s, out var result))
					{
						dictionary[text2] = result;
					}
				}
			}
			return dictionary;
		}

		private static Dictionary<string, float> ParseStringFloatMap(string json, string key)
		{
			Dictionary<string, float> dictionary = new Dictionary<string, float>();
			string text = Companion.ExtractJsonField(json, key, isComplex: true);
			if (string.IsNullOrEmpty(text) || text.Length < 2)
			{
				return dictionary;
			}
			foreach (string item in SplitTopLevel(text.Substring(1, text.Length - 2)))
			{
				int num = item.IndexOf(':');
				if (num >= 0)
				{
					string text2 = item.Substring(0, num).Trim().Trim(new char[1] { '"' });
					string s = item.Substring(num + 1).Trim();
					if (!string.IsNullOrEmpty(text2) && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && !float.IsNaN(result) && !float.IsInfinity(result))
					{
						dictionary[text2] = result;
					}
				}
			}
			return dictionary;
		}

		private static Dictionary<string, int> ParseStringIntMap(string json, string key)
		{
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			string text = Companion.ExtractJsonField(json, key, isComplex: true);
			if (string.IsNullOrEmpty(text) || text.Length < 2)
			{
				return dictionary;
			}
			foreach (string item in SplitTopLevel(text.Substring(1, text.Length - 2)))
			{
				int num = item.IndexOf(':');
				if (num >= 0)
				{
					string text2 = item.Substring(0, num).Trim().Trim(new char[1] { '"' });
					string s = item.Substring(num + 1).Trim();
					if (!string.IsNullOrEmpty(text2) && int.TryParse(s, out var result))
					{
						dictionary[text2] = result;
					}
				}
			}
			return dictionary;
		}

		private static IEnumerable<string> SplitTopLevel(string s)
		{
			List<string> list = new List<string>();
			int num = 0;
			bool flag = false;
			bool flag2 = false;
			int num2 = 0;
			for (int i = 0; i < s.Length; i++)
			{
				char c = s[i];
				if (flag2)
				{
					flag2 = false;
				}
				else if (c == '\\' && flag)
				{
					flag2 = true;
				}
				else if (c == '"')
				{
					flag = !flag;
				}
				else
				{
					if (flag)
					{
						continue;
					}
					switch (c)
					{
					case '[':
					case '{':
						num++;
						break;
					case ']':
					case '}':
						num--;
						break;
					case ',':
						if (num == 0)
						{
							list.Add(s.Substring(num2, i - num2));
							num2 = i + 1;
						}
						break;
					}
				}
			}
			if (num2 < s.Length)
			{
				list.Add(s.Substring(num2));
			}
			return list;
		}

		public static void Save(PlayerRecord rec)
		{
			if (rec == null)
			{
				return;
			}
			try
			{
				Directory.CreateDirectory(PlayersDir);
				AtomicWrite(SanitizedFileName(rec.Name), ToJson(rec));
				rec.Dirty = false;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[TheRavensCall] PlayerRegistry save failed for " + rec?.Name + ": " + ex.Message));
			}
		}

		public static void SaveAll()
		{
			foreach (PlayerRecord value in _records.Values)
			{
				Save(value);
			}
		}

		public static void SaveDirty()
		{
			foreach (PlayerRecord value in _records.Values)
			{
				if (value.Dirty)
				{
					Save(value);
				}
			}
		}

		public static string ToJson(PlayerRecord r)
		{
			string text = "{" + string.Join(",", r.CreatureKills.Select((KeyValuePair<string, int> kv) => "\"" + Companion.Esc(kv.Key) + "\":" + kv.Value)) + "}";
			string text2 = "[" + string.Join(",", r.DeathHistory) + "]";
			string text3 = "{" + string.Join(",", r.VanillaStats.Select((KeyValuePair<string, float> kv) => "\"" + Companion.Esc(kv.Key) + "\":" + Companion.F(kv.Value))) + "}";
			string text4 = "{" + string.Join(",", r.SkillLevels.Select((KeyValuePair<string, float> kv) => "\"" + Companion.Esc(kv.Key) + "\":" + Companion.F(kv.Value))) + "}";
			string text5 = "{" + string.Join(",", r.SkillProgress.Select((KeyValuePair<string, float> kv) => "\"" + Companion.Esc(kv.Key) + "\":" + Companion.F(kv.Value))) + "}";
			string text6 = "{" + string.Join(",", r.ResourcesHarvested.Select((KeyValuePair<string, int> kv) => "\"" + Companion.Esc(kv.Key) + "\":" + kv.Value)) + "}";
			StringBuilder stringBuilder = new StringBuilder("{");
			stringBuilder.Append("\"name\":\"" + Companion.Esc(r.Name) + "\",");
			stringBuilder.Append("\"first_seen\":\"" + Companion.Esc(r.FirstSeen) + "\",");
			stringBuilder.Append("\"last_seen\":\"" + Companion.Esc(r.LastSeen) + "\",");
			stringBuilder.Append("\"online\":" + Companion.B(r.Online) + ",");
			stringBuilder.Append("\"session_start\":" + ((r.SessionStart != null) ? ("\"" + Companion.Esc(r.SessionStart) + "\"") : "null") + ",");
			stringBuilder.Append("\"playtime_seconds_lifetime\":" + r.PlaytimeSecondsLifetime + ",");
			stringBuilder.Append("\"kills_narrative\":" + r.Kills + ",");
			stringBuilder.Append("\"deaths_narrative\":" + r.Deaths + ",");
			stringBuilder.Append("\"gear_tier\":" + r.GearTier + ",");
			stringBuilder.Append("\"active_title\":\"" + Companion.Esc(r.ActiveTitle) + "\",");
			stringBuilder.Append("\"titles_earned\":[" + string.Join(",", r.EarnedTitles.Select((string t) => "\"" + Companion.Esc(t) + "\"")) + "],");
			stringBuilder.Append("\"biomes_discovered\":[" + string.Join(",", r.BiomesDiscovered.Select((string b) => "\"" + Companion.Esc(b) + "\"")) + "],");
			stringBuilder.Append("\"boss_kills_credited\":" + r.BossKillsCredited + ",");
			stringBuilder.Append("\"creature_kills\":" + text + ",");
			stringBuilder.Append("\"session_kills\":" + r.SessionKills + ",");
			stringBuilder.Append("\"damage_dealt_session\":" + Companion.F(r.SessionDmgDone) + ",");
			stringBuilder.Append("\"damage_taken_session\":" + Companion.F(r.SessionDmgTaken) + ",");
			stringBuilder.Append("\"blocks_session\":" + r.SessionBlocks + ",");
			stringBuilder.Append("\"parries_session\":" + r.SessionParries + ",");
			stringBuilder.Append("\"damage_blocked_session\":" + Companion.F(r.SessionDmgBlocked) + ",");
			stringBuilder.Append("\"kills_observed_lifetime\":" + r.TotalKillsLifetime + ",");
			stringBuilder.Append("\"deaths_lifetime\":" + r.TotalDeathsLifetime + ",");
			stringBuilder.Append("\"bosses_defeated\":[" + string.Join(",", r.DefeatedBosses.Select((string b) => "\"" + Companion.Esc(b) + "\"")) + "],");
			stringBuilder.Append("\"caught_fish\":[" + string.Join(",", r.CaughtFish.Select((string f) => "\"" + Companion.Esc(f) + "\"")) + "],");
			stringBuilder.Append("\"death_history\":" + text2 + ",");
			stringBuilder.Append("\"vanilla_stats\":" + text3 + ",");
			stringBuilder.Append("\"skill_levels\":" + text4 + ",");
			stringBuilder.Append("\"skill_progress\":" + text5 + ",");
			stringBuilder.Append("\"builds_placed\":" + r.BuildsPlaced + ",");
			stringBuilder.Append("\"builds_removed\":" + r.BuildsRemoved + ",");
			stringBuilder.Append("\"builds_repaired\":" + r.BuildsRepaired + ",");
			stringBuilder.Append("\"items_crafted\":" + r.ItemsCrafted + ",");
			stringBuilder.Append("\"items_upgraded\":" + r.ItemsUpgraded + ",");
			stringBuilder.Append("\"items_repaired\":" + r.ItemsRepaired + ",");
			stringBuilder.Append("\"resources_harvested\":" + text6 + ",");
			stringBuilder.Append("\"consumables_eaten\":" + r.ConsumablesEaten + ",");
			stringBuilder.Append("\"bosses_summoned\":" + r.BossesSummoned + ",");
			stringBuilder.Append("\"guardian_powers_used\":" + r.GuardianPowersUsed);
			stringBuilder.Append("}");
			return stringBuilder.ToString();
		}

		public static string BuildBarrkBotJson(string worldName, int day, int onlineCount)
		{
			StringBuilder stringBuilder = new StringBuilder("{");
			stringBuilder.Append("\"generated_at\":\"" + DateTime.UtcNow.ToString("o") + "\",");
			stringBuilder.Append("\"world_name\":\"" + Companion.Esc(worldName) + "\",");
			stringBuilder.Append("\"day\":" + day + ",");
			stringBuilder.Append("\"online_count\":" + onlineCount + ",");
			stringBuilder.Append("\"raid_active\":" + Companion.B(WorldState.RaidActive) + ",");
			stringBuilder.Append("\"raid_type\":\"" + Companion.Esc(WorldState.RaidType) + "\",");
			stringBuilder.Append("\"players\":{");
			stringBuilder.Append(string.Join(",", from r in All.OrderBy<PlayerRecord, string>((PlayerRecord r) => r.Name, StringComparer.OrdinalIgnoreCase)
				select "\"" + Companion.Esc(r.Name) + "\":" + ToJson(r)));
			stringBuilder.Append("}}");
			return stringBuilder.ToString();
		}
	}
	internal static class PushClient
	{
		private sealed class PushOutcome
		{
			internal enum Kind
			{
				Success,
				RateLimited,
				ConfigRefusal,
				OtherFailure
			}

			internal Kind ResultKind;

			internal DateTime AttemptUtc;

			internal bool[] EnvelopeSent;

			internal string[] BlankedForms;

			internal int? RetryAfterSeconds;

			internal string ConfigRefusalMessage;

			internal string ErrorText;
		}

		internal static volatile bool Enabled;

		private static readonly Regex ServerIdPattern = new Regex("^[a-z0-9-]{1,32}$", RegexOptions.Compiled);

		private static readonly Regex GeneratedAtField = new Regex("\"generated_at\":\"[^\"]*\"", RegexOptions.Compiled);

		private static readonly Regex DurationMsField = new Regex("\"duration_ms\":-?[0-9]+", RegexOptions.Compiled);

		private static readonly Regex ScannedObjectsField = new Regex("\"scanned_objects\":-?[0-9]+", RegexOptions.Compiled);

		private const long MaxBodyBytes = 2097152L;

		private static string _pushUrl;

		private static string _pushToken;

		private static string _serverId;

		private static string _readTokenSha256;

		private static int _effectiveIntervalSeconds;

		private static int _effectiveHeartbeatSeconds;

		private static DateTime _lastAttemptUtc = DateTime.MinValue;

		private static DateTime _lastSuccessUtc = DateTime.MinValue;

		private static DateTime _nextAllowedAttemptUtc = DateTime.MinValue;

		private static volatile bool _requestInFlight;

		private static string _lastAcceptedStateBlanked;

		private static string _lastAcceptedActivityBlanked;

		private static string _lastAcceptedCensusBlanked;

		private static int _consecutiveFailures;

		private static DateTime _lastFailureLogUtc = DateTime.MinValue;

		private static DateTime _lastOversizeWarnUtc = DateTime.MinValue;

		private static string _lastConfigRefusalMessageLogged;

		private static bool _forceAllNextAttempt;

		private static readonly object _outcomeLock = new object();

		private static PushOutcome _pendingOutcome;

		private static int Clamp(int value, int min, int max)
		{
			return Math.Max(min, Math.Min(max, value));
		}

		internal static void Init()
		{
			string text = Plugin.PushUrl.Value?.Trim();
			if (string.IsNullOrEmpty(text))
			{
				Enabled = false;
				return;
			}
			bool flag = true;
			if (!Uri.TryCreate(text, UriKind.Absolute, out Uri result) || (!string.Equals(result.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && (!string.Equals(result.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || !result.IsLoopback)))
			{
				Plugin.Log.LogWarning((object)("[TheRavensCall] Push disabled: [Push] PushUrl '" + text + "' must be an https URL (http is accepted only to a loopback host, for local testing against netlify dev) — fix the URL and restart."));
				flag = false;
			}
			string text2 = ((Plugin.HttpApiToken != null) ? (Plugin.HttpApiToken.Value ?? "") : "");
			if (text2.Length < 24)
			{
				Plugin.Log.LogWarning((object)"[TheRavensCall] Push disabled: a hosted dashboard needs [Companion] HttpApiToken to be at least 24 characters — set a longer random token and restart.");
				flag = false;
			}
			string text3 = Plugin.PushServerId.Value?.Trim() ?? "";
			if (!ServerIdPattern.IsMatch(text3))
			{
				Plugin.Log.LogWarning((object)"[TheRavensCall] Push disabled: [Push] PushServerId must be 1-32 characters of a-z, 0-9 and '-', matching the id registered in the receiver's TRC_SERVERS — set it and restart.");
				flag = false;
			}
			string text4 = Plugin.PushToken.Value ?? "";
			if (string.IsNullOrEmpty(text4))
			{
				Plugin.Log.LogWarning((object)"[TheRavensCall] Push disabled: [Push] PushToken is empty — set the write token registered for this server in the receiver and restart.");
				flag = false;
			}
			else if (text4.Length < 32)
			{
				Plugin.Log.LogWarning((object)"[TheRavensCall] [Push] PushToken is under 32 characters — generate a longer random token (e.g. `openssl rand -hex 24`) before pointing this server at a public receiver.");
			}
			if (!flag)
			{
				Enabled = false;
				return;
			}
			_pushUrl = text;
			_pushToken = text4;
			_serverId = text3;
			using (SHA256 sHA = SHA256.Create())
			{
				byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(text2));
				StringBuilder stringBuilder = new StringBuilder(array.Length * 2);
				for (int i = 0; i < array.Length; i++)
				{
					stringBuilder.Append(array[i].ToString("x2", CultureInfo.InvariantCulture));
				}
				_readTokenSha256 = stringBuilder.ToString();
			}
			int num = Clamp(Plugin.PushIntervalSeconds.Value, 15, 3600);
			float num2 = ((Plugin.StatsPushIntervalSeconds != null && Plugin.StatsPushIntervalSeconds.Value > 0f) ? Plugin.StatsPushIntervalSeconds.Value : 10f);
			_effectiveIntervalSeconds = (int)(Math.Ceiling((double)num / (double)num2) * (double)num2);
			int num3 = Clamp(Plugin.PushHeartbeatMinutes.Value, 1, 1440) * 60;
			_effectiveHeartbeatSeconds = Math.Max(num3, _effectiveIntervalSeconds);
			if (_effectiveHeartbeatSeconds > num3)
			{
				Plugin.Log.LogWarning((object)("[TheRavensCall] [Push] PushHeartbeatMinutes raised so the heartbeat (" + _effectiveHeartbeatSeconds.ToString(CultureInfo.InvariantCulture) + "s) is never shorter than the effective push interval (" + _effectiveIntervalSeconds.ToString(CultureInfo.InvariantCulture) + "s)."));
			}
			Enabled = true;
			Plugin.Log.LogInfo((object)("[TheRavensCall] Push enabled: server_id='" + _serverId + "', url=" + _pushUrl + ", interval=" + _effectiveIntervalSeconds.ToString(CultureInfo.InvariantCulture) + "s, heartbeat=" + _effectiveHeartbeatSeconds.ToString(CultureInfo.InvariantCulture) + "s."));
		}

		private static string BlankGeneratedAt(string json)
		{
			return GeneratedAtField.Replace(json, "\"generated_at\":\"\"", 1);
		}

		private static string BlankCensusVolatile(string json)
		{
			json = BlankGeneratedAt(json);
			json = DurationMsField.Replace(json, "\"duration_ms\":0", 1);
			json = ScannedObjectsField.Replace(json, "\"scanned_objects\":0", 1);
			return json;
		}

		internal static void MaybeSend()
		{
			if (!Enabled)
			{
				return;
			}
			try
			{
				if (_requestInFlight)
				{
					return;
				}
				DrainOutcome();
				DateTime utcNow = DateTime.UtcNow;
				if (utcNow < _lastAttemptUtc || utcNow < _lastSuccessUtc)
				{
					_lastAttemptUtc = DateTime.MinValue;
					_lastSuccessUtc = DateTime.MinValue;
					_nextAllowedAttemptUtc = DateTime.MinValue;
					Plugin.Log.LogWarning((object)"[TheRavensCall] System clock stepped backwards — push schedule reset.");
				}
				if ((_lastAttemptUtc != DateTime.MinValue && (utcNow - _lastAttemptUtc).TotalSeconds < (double)_effectiveIntervalSeconds) || utcNow < _nextAllowedAttemptUtc)
				{
					return;
				}
				_lastAttemptUtc = utcNow;
				string text = Companion._stateCache ?? "{\"generated_at\":\"\",\"world_name\":\"\",\"day\":0,\"online_count\":0,\"raid_active\":false,\"raid_type\":\"\",\"players\":{}}";
				string text2 = Companion._activityCache ?? "{\"generated_at\":\"\",\"season\":{\"active\":false,\"name\":\"\",\"started_at\":null,\"standings_since\":null,\"last_ended\":null,\"last_ended_at\":null,\"standings\":[]},\"events\":[]}";
				string text3 = Companion._censusCache ?? WorldCensus.EmptyEnvelope();
				string text4 = BlankGeneratedAt(text);
				string text5 = BlankGeneratedAt(text2);
				string text6 = BlankCensusVolatile(text3);
				int num;
				int num2;
				if (!(_lastSuccessUtc == DateTime.MinValue))
				{
					num = (((utcNow - _lastSuccessUtc).TotalSeconds >= (double)_effectiveHeartbeatSeconds) ? 1 : 0);
					if (num == 0 && !_forceAllNextAttempt)
					{
						num2 = ((!string.Equals(text4, _lastAcceptedStateBlanked, StringComparison.Ordinal)) ? 1 : 0);
						goto IL_0158;
					}
				}
				else
				{
					num = 1;
				}
				num2 = 1;
				goto IL_0158;
				IL_0158:
				bool flag = (byte)num2 != 0;
				bool flag2 = num != 0 || _forceAllNextAttempt || !string.Equals(text5, _lastAcceptedActivityBlanked, StringComparison.Ordinal);
				bool flag3 = num != 0 || _forceAllNextAttempt || !string.Equals(text6, _lastAcceptedCensusBlanked, StringComparison.Ordinal);
				if (!flag && !flag2 && !flag3)
				{
					return;
				}
				string stateField = (flag ? ("\"" + Companion.Esc(text) + "\"") : "null");
				string activityField = (flag2 ? ("\"" + Companion.Esc(text2) + "\"") : "null");
				string censusField = (flag3 ? ("\"" + Companion.Esc(text3) + "\"") : "null");
				string pushedAt = utcNow.ToString("o", CultureInfo.InvariantCulture);
				string text7 = BuildPayload(pushedAt, stateField, activityField, censusField);
				long num3 = Encoding.UTF8.GetByteCount(text7);
				if (num3 > 2097152)
				{
					long num4 = (flag ? Encoding.UTF8.GetByteCount(text) : (-1));
					long num5 = (flag2 ? Encoding.UTF8.GetByteCount(text2) : (-1));
					long num6 = (flag3 ? Encoding.UTF8.GetByteCount(text3) : (-1));
					if (num4 >= num5 && num4 >= num6 && flag)
					{
						flag = false;
						stateField = "null";
					}
					else if (num5 >= num4 && num5 >= num6 && flag2)
					{
						flag2 = false;
						activityField = "null";
					}
					else if (flag3)
					{
						flag3 = false;
						censusField = "null";
					}
					if (!flag && !flag2 && !flag3)
					{
						if (_lastOversizeWarnUtc == DateTime.MinValue || (utcNow - _lastOversizeWarnUtc).TotalHours >= 1.0)
						{
							Plugin.Log.LogWarning((object)("[TheRavensCall] Push bundle for '" + _serverId + "' was " + num3.ToString(CultureInfo.InvariantCulture) + " bytes, over the receiver's 2 MB limit — dropped entirely (nothing sent) this attempt."));
							_lastOversizeWarnUtc = utcNow;
						}
						return;
					}
					text7 = BuildPayload(pushedAt, stateField, activityField, censusField);
					if (_lastOversizeWarnUtc == DateTime.MinValue || (utcNow - _lastOversizeWarnUtc).TotalHours >= 1.0)
					{
						Plugin.Log.LogWarning((object)("[TheRavensCall] Push bundle for '" + _serverId + "' was " + num3.ToString(CultureInfo.InvariantCulture) + " bytes, over the receiver's 2 MB limit — dropped the largest envelope this attempt."));
						_lastOversizeWarnUtc = utcNow;
					}
				}
				bool[] sentFlags = new bool[3] { flag, flag2, flag3 };
				string[] blankedForms = new string[3] { text4, text5, text6 };
				_requestInFlight = true;
				string url = _pushUrl;
				string token = _pushToken;
				string body = text7;
				DateTime attemptUtc = utcNow;
				_forceAllNextAttempt = false;
				ThreadPool.QueueUserWorkItem(delegate
				{
					SendWorker(url, token, body, attemptUtc, sentFlags, blankedForms);
				});
			}
			catch (Exception ex)
			{
				_requestInFlight = false;
				Plugin.Log.LogError((object)("[TheRavensCall] Push tick failed: " + ex.Message));
			}
		}

		private static string BuildPayload(string pushedAt, string stateField, string activityField, string censusField)
		{
			return "{\"v\":1,\"server_id\":\"" + Companion.Esc(_serverId) + "\",\"mod_version\":\"1.7.2\",\"pushed_at\":\"" + pushedAt + "\",\"push_interval_seconds\":" + _effectiveIntervalSeconds.ToString(CultureInfo.InvariantCulture) + ",\"heartbeat_seconds\":" + _effectiveHeartbeatSeconds.ToString(CultureInfo.InvariantCulture) + ",\"read_token_sha256\":\"" + _readTokenSha256 + "\",\"state\":" + stateField + ",\"activity\":" + activityField + ",\"census\":" + censusField + "}";
		}

		private static void SendWorker(string pushUrl, string pushToken, string payload, DateTime attemptUtc, bool[] sentFlags, string[] blankedForms)
		{
			PushOutcome pushOutcome = new PushOutcome
			{
				AttemptUtc = attemptUtc,
				EnvelopeSent = sentFlags,
				BlankedForms = blankedForms
			};
			try
			{
				HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create((pushUrl.EndsWith("/", StringComparison.Ordinal) ? pushUrl.Substring(0, pushUrl.Length - 1) : pushUrl) + "/push");
				httpWebRequest.Method = "POST";
				httpWebRequest.ContentType = "application/json";
				httpWebRequest.Headers[HttpRequestHeader.Authorization] = "Bearer " + pushToken;
				httpWebRequest.UserAgent = "TheRavensCall/1.6.0";
				httpWebRequest.Timeout = 20000;
				httpWebRequest.ReadWriteTimeout = 20000;
				byte[] bytes = Encoding.UTF8.GetBytes(payload);
				httpWebRequest.ContentLength = bytes.Length;
				using (Stream stream = httpWebRequest.GetRequestStream())
				{
					stream.Write(bytes, 0, bytes.Length);
				}
				using ((HttpWebResponse)httpWebRequest.GetResponse())
				{
					pushOutcome.ResultKind = PushOutcome.Kind.Success;
				}
			}
			catch (WebException ex)
			{
				using HttpWebResponse httpWebResponse = ex.Response as HttpWebResponse;
				if (httpWebResponse == null)
				{
					pushOutcome.ResultKind = PushOutcome.Kind.OtherFailure;
					if (ex.Status == WebExceptionStatus.TrustFailure || ex.Status == WebExceptionStatus.SecureChannelFailure)
					{
						pushOutcome.ErrorText = "TLS certificate validation failed (" + ex.Status.ToString() + ") — the receiver's certificate is not trusted by this machine's CA bundle";
					}
					else
					{
						pushOutcome.ErrorText = ex.Status.ToString() + ": " + ex.Message;
					}
				}
				else
				{
					int statusCode = (int)httpWebResponse.StatusCode;
					switch (statusCode)
					{
					case 429:
					{
						pushOutcome.ResultKind = PushOutcome.Kind.RateLimited;
						string s = httpWebResponse.Headers["Retry-After"];
						pushOutcome.RetryAfterSeconds = (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? new int?(result) : ((int?)null));
						break;
					}
					case 401:
						pushOutcome.ResultKind = PushOutcome.Kind.ConfigRefusal;
						pushOutcome.ConfigRefusalMessage = "PushToken rejected — check [Push] PushToken and PushServerId against the receiver's registry";
						break;
					case 409:
						pushOutcome.ResultKind = PushOutcome.Kind.ConfigRefusal;
						pushOutcome.ConfigRefusalMessage = "the receiver has a different HttpApiToken hash registered for '" + _serverId + "' — re-register read_token_sha256 and redeploy";
						break;
					case 413:
						pushOutcome.ResultKind = PushOutcome.Kind.ConfigRefusal;
						pushOutcome.ConfigRefusalMessage = "bundle over the receiver's 2 MB limit";
						break;
					case 422:
						pushOutcome.ResultKind = PushOutcome.Kind.ConfigRefusal;
						pushOutcome.ConfigRefusalMessage = "the receiver refused the bundle — set [Companion] HttpApiToken (24+ characters)";
						break;
					default:
						pushOutcome.ResultKind = PushOutcome.Kind.OtherFailure;
						pushOutcome.ErrorText = "HTTP " + statusCode.ToString(CultureInfo.InvariantCulture);
						break;
					}
				}
			}
			catch (Exception ex2)
			{
				pushOutcome.ResultKind = PushOutcome.Kind.OtherFailure;
				pushOutcome.ErrorText = ex2.Message;
			}
			lock (_outcomeLock)
			{
				_pendingOutcome = pushOutcome;
			}
			_requestInFlight = false;
		}

		private static int BackoffSecondsFor(int consecutiveFailures)
		{
			return consecutiveFailures switch
			{
				1 => 60, 
				2 => 120, 
				3 => 240, 
				4 => 480, 
				_ => 900, 
			};
		}

		private static void DrainOutcome()
		{
			PushOutcome pendingOutcome;
			lock (_outcomeLock)
			{
				pendingOutcome = _pendingOutcome;
				_pendingOutcome = null;
			}
			if (pendingOutcome == null)
			{
				return;
			}
			switch (pendingOutcome.ResultKind)
			{
			case PushOutcome.Kind.Success:
			{
				if (pendingOutcome.EnvelopeSent[0])
				{
					_lastAcceptedStateBlanked = pendingOutcome.BlankedForms[0];
				}
				if (pendingOutcome.EnvelopeSent[1])
				{
					_lastAcceptedActivityBlanked = pendingOutcome.BlankedForms[1];
				}
				if (pendingOutcome.EnvelopeSent[2])
				{
					_lastAcceptedCensusBlanked = pendingOutcome.BlankedForms[2];
				}
				int consecutiveFailures = _consecutiveFailures;
				_consecutiveFailures = 0;
				_nextAllowedAttemptUtc = DateTime.MinValue;
				_lastSuccessUtc = pendingOutcome.AttemptUtc;
				if (consecutiveFailures > 0)
				{
					Plugin.Log.LogInfo((object)("[TheRavensCall] Push to the receiver recovered after " + consecutiveFailures.ToString(CultureInfo.InvariantCulture) + " failed attempt(s)."));
				}
				break;
			}
			case PushOutcome.Kind.RateLimited:
				if (pendingOutcome.RetryAfterSeconds.HasValue && pendingOutcome.RetryAfterSeconds.Value > 0)
				{
					int value = pendingOutcome.RetryAfterSeconds.Value;
					int num2 = Math.Min(value, 900);
					_nextAllowedAttemptUtc = pendingOutcome.AttemptUtc.AddSeconds(num2);
					if (value > num2 && (pendingOutcome.AttemptUtc - _lastFailureLogUtc).TotalHours >= 1.0)
					{
						Plugin.Log.LogInfo((object)("[TheRavensCall] Push rate-limited: receiver asked for a " + value.ToString(CultureInfo.InvariantCulture) + "s wait, clamped to " + num2.ToString(CultureInfo.InvariantCulture) + "s."));
						_lastFailureLogUtc = pendingOutcome.AttemptUtc;
					}
				}
				break;
			case PushOutcome.Kind.ConfigRefusal:
				_nextAllowedAttemptUtc = pendingOutcome.AttemptUtc.AddMinutes(15.0);
				_forceAllNextAttempt = true;
				_consecutiveFailures = 0;
				if (!string.Equals(pendingOutcome.ConfigRefusalMessage, _lastConfigRefusalMessageLogged, StringComparison.Ordinal) || (pendingOutcome.AttemptUtc - _lastFailureLogUtc).TotalHours >= 1.0)
				{
					Plugin.Log.LogWarning((object)("[TheRavensCall] Push rejected: " + pendingOutcome.ConfigRefusalMessage));
					_lastFailureLogUtc = pendingOutcome.AttemptUtc;
					_lastConfigRefusalMessageLogged = pendingOutcome.ConfigRefusalMessage;
				}
				break;
			case PushOutcome.Kind.OtherFailure:
			{
				_consecutiveFailures++;
				_forceAllNextAttempt = true;
				int num = BackoffSecondsFor(_consecutiveFailures);
				_nextAllowedAttemptUtc = pendingOutcome.AttemptUtc.AddSeconds(num);
				if (_consecutiveFailures == 1)
				{
					Plugin.Log.LogWarning((object)("[TheRavensCall] Push failed: " + pendingOutcome.ErrorText + " — retrying in " + num.ToString(CultureInfo.InvariantCulture) + "s."));
					_lastFailureLogUtc = pendingOutcome.AttemptUtc;
				}
				else if ((pendingOutcome.AttemptUtc - _lastFailureLogUtc).TotalHours >= 1.0)
				{
					Plugin.Log.LogWarning((object)("[TheRavensCall] Push still failing (" + _consecutiveFailures.ToString(CultureInfo.InvariantCulture) + " in a row) — last success " + ((_lastSuccessUtc == DateTime.MinValue) ? "never" : _lastSuccessUtc.ToString("o", CultureInfo.InvariantCulture)) + ", last error: " + pendingOutcome.ErrorText));
					_lastFailureLogUtc = pendingOutcome.AttemptUtc;
				}
				break;
			}
			}
		}
	}
	[BepInPlugin("com.raveniron.theravenscall", "TheRavensCall", "1.7.2")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGUID = "com.raveniron.theravenscall";

		public const string PluginName = "TheRavensCall";

		public const string PluginVersion = "1.7.2";

		internal static ManualLogSource Log;

		internal static Plugin Instance;

		internal static bool WasServer;

		private readonly Harmony _harmony = new Harmony("com.raveniron.theravenscall");

		public static ConfigEntry<bool> EnablePlayerDeath;

		public static ConfigEntry<bool> EnableBossKill;

		public static ConfigEntry<bool> EnablePlayerJoin;

		public static ConfigEntry<bool> EnablePlayerLeave;

		public static ConfigEntry<bool> EnableBiomeDiscovery;

		public static ConfigEntry<bool> EnableKillMilestone;

		public static ConfigEntry<bool> EnableDeathMilestone;

		public static ConfigEntry<bool> EnableGearTier;

		public static ConfigEntry<bool> EnableTitleEarned;

		public static ConfigEntry<bool> EnableRaid;

		public static ConfigEntry<bool> ShowDayNumber;

		public static ConfigEntry<bool> ShowOnlineCount;

		public static ConfigEntry<string> MessagePrefix;

		public static ConfigEntry<bool> EnableChronicleLog;

		public static ConfigEntry<bool> LogServerStartStop;

		public static ConfigEntry<float> BossCreditRadius;

		public static ConfigEntry<string> DiscordWebhookUrl;

		public static ConfigEntry<bool> EnableNarrativeMode;

		public static ConfigEntry<bool> AppendSeasonNameToMessages;

		public static ConfigEntry<string> WorldEventEikthyr;

		public static ConfigEntry<string> WorldEventElder;

		public static ConfigEntry<string> WorldEventBonemass;

		public static ConfigEntry<string> WorldEventModer;

		public static ConfigEntry<string> WorldEventYagluth;

		public static ConfigEntry<string> WorldEventQueen;

		public static ConfigEntry<string> WorldEventFader;

		public static ConfigEntry<int> LoreBroadcastIntervalMinutes;

		public static ConfigEntry<bool> EnableHttpServer;

		public static ConfigEntry<int> HttpServerPort;

		public static ConfigEntry<float> StatsPushIntervalSeconds;

		public static ConfigEntry<bool> HttpBindAllInterfaces;

		public static ConfigEntry<string> HttpApiToken;

		public static ConfigEntry<int> EventFeedCapacity;

		public static ConfigEntry<string> PushUrl;

		public static ConfigEntry<string> PushToken;

		public static ConfigEntry<string> PushServerId;

		public static ConfigEntry<int> PushIntervalSeconds;

		public static ConfigEntry<int> PushHeartbeatMinutes;

		public static ConfigEntry<int> CensusIntervalMinutes;

		public static ConfigEntry<bool> AcceptClientReports;

		public static ConfigEntry<bool> LogCombatReports;

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			Instance = this;
			EnablePlayerDeath = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnablePlayerDeath", true, "Log/announce when a player dies");
			EnableBossKill = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableBossKill", true, "Log/announce when a boss is killed");
			EnablePlayerJoin = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnablePlayerJoin", true, "Log when a player joins");
			EnablePlayerLeave = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnablePlayerLeave", true, "Log when a player leaves");
			EnableBiomeDiscovery = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableBiomeDiscovery", true, "Log/announce when a player discovers a new biome");
			EnableKillMilestone = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableKillMilestone", true, "Log/announce enemy kill milestones");
			EnableDeathMilestone = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableDeathMilestone", true, "Log/announce death milestones");
			EnableGearTier = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableGearTier", true, "Log/announce new gear tier reached");
			EnableTitleEarned = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableTitleEarned", true, "Log/announce when a title is earned");
			EnableRaid = ((BaseUnityPlugin)this).Config.Bind<bool>("Events", "EnableRaid", true, "Record raid start and end in the Chronicle and the dashboard feed (raid_start / raid_end). Not posted to Discord. The raid banner and raid_active work either way.");
			ShowDayNumber = ((BaseUnityPlugin)this).Config.Bind<bool>("Format", "ShowDayNumber", true, "Append day number to messages");
			ShowOnlineCount = ((BaseUnityPlugin)this).Config.Bind<bool>("Format", "ShowOnlineCount", true, "Append online count to messages");
			MessagePrefix = ((BaseUnityPlugin)this).Config.Bind<string>("Format", "MessagePrefix", "⚔ ", "Prefix for all narrated messages (Discord + Chronicle)");
			EnableChronicleLog = ((BaseUnityPlugin)this).Config.Bind<bool>("Log", "EnableChronicleLog", true, "Write TheRavensCall_Chronicle.log");
			LogServerStartStop = ((BaseUnityPlugin)this).Config.Bind<bool>("Log", "LogServerStartStop", true, "Log server start/stop events to Chronicle");
			BossCreditRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bosses", "BossCreditRadius", 100f, "Radius in meters around a boss's death position where players receive kill credit");
			DiscordWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "WebhookUrl", "", "Discord webhook URL for event notifications. Leave empty to disable");
			EnableNarrativeMode = ((BaseUnityPlugin)this).Config.Bind<bool>("Narrative", "EnableNarrativeMode", true, "Use randomized narrative message templates instead of fixed strings");
			AppendSeasonNameToMessages = ((BaseUnityPlugin)this).Config.Bind<bool>("Seasons", "AppendSeasonNameToMessages", false, "Prepend the active season name to all narrated messages");
			WorldEventEikthyr = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "Eikthyr", "", "Message posted after Eikthyr is defeated. Leave empty to disable");
			WorldEventElder = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "TheElder", "", "Message posted after The Elder is defeated. Leave empty to disable");
			WorldEventBonemass = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "Bonemass", "", "Message posted after Bonemass is defeated. Leave empty to disable");
			WorldEventModer = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "Moder", "", "Message posted after Moder is defeated. Leave empty to disable");
			WorldEventYagluth = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "Yagluth", "", "Message posted after Yagluth is defeated. Leave empty to disable");
			WorldEventQueen = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "TheQueen", "", "Message posted after The Queen is defeated. Leave empty to disable");
			WorldEventFader = ((BaseUnityPlugin)this).Config.Bind<string>("WorldEvents", "Fader", "", "Message posted after Fader is defeated. Leave empty to disable");
			LoreBroadcastIntervalMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("Lore", "LoreBroadcastIntervalMinutes", 0, "Post a random lore.txt entry to Discord/Chronicle at this interval in minutes. 0 = disabled");
			EnableHttpServer = ((BaseUnityPlugin)this).Config.Bind<bool>("Companion", "EnableHttpServer", true, "Run the local HTTP dashboard/API on this machine (port below)");
			HttpServerPort = ((BaseUnityPlugin)this).Config.Bind<int>("Companion", "HttpServerPort", 2112, "Port for the HTTP dashboard/API (/api/state is exactly the BarrkBOT export, the same JSON written to BarrkBOT_data1.json)");
			StatsPushIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Companion", "StatsPushIntervalSeconds", 10f, "How often (seconds) to snapshot every online player's state, check for new biomes/gear tiers, and refresh the BarrkBOT export file");
			HttpBindAllInterfaces = ((BaseUnityPlugin)this).Config.Bind<bool>("Companion", "HttpBindAllInterfaces", false, "Also listen on every network interface (http://+:port), not only localhost. Off by default since 1.2.4: the API hands every known player's stats, skills, titles and death coordinates to anyone who can reach the port, with no login. Turn on only behind a firewall or together with HttpApiToken");
			HttpApiToken = ((BaseUnityPlugin)this).Config.Bind<string>("Companion", "HttpApiToken", "", "If set, /api/state, /api/gamedata, /api/pins, /api/activity and /api/census require ?token=<this value> (or an X-Api-Token header). /api/health and the dashboard page stay open. Since 1.3.0 the bundled dashboard page has a settings panel (gear icon) to enter this token itself, stored in the browser and sent as X-Api-Token — a 401 opens that panel automatically. A pre-1.3.0 page still does not send a token. Changing this while the server runs does not update the read-token hash pushed to a hosted receiver — restart the server and re-register read_token_sha256 in the receiver's TRC_SERVERS");
			EventFeedCapacity = ((BaseUnityPlugin)this).Config.Bind<int>("Companion", "EventFeedCapacity", 200, "Number of recent Chronicle lines kept in memory and served by /api/activity, newest first (0 to 1000). They are saved to event_feed.json so the feed survives a restart. 0 turns the feed off; the endpoint still answers, with an empty events array, so a 1.4.0 dashboard can tell \"turned off\" from \"older server\".");
			PushUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Push", "PushUrl", "", "Base URL of a hosted receiver (see docs/API.md) that this server pushes its /api/state, /api/activity and /api/census envelopes to, e.g. https://dash.example.com. Leave empty (the default) to disable pushing entirely — 1.6.0 then behaves exactly like 1.5.0. Must be https, or http only to a loopback host (for local testing against `netlify dev`); anything else disables the push at boot with a warning naming the host.");
			PushToken = ((BaseUnityPlugin)this).Config.Bind<string>("Push", "PushToken", "", "The write token this server pushes with — generate one randomly (e.g. `openssl rand -hex 24`) and register its sha256 as this PushServerId's 'w' in the receiver's TRC_SERVERS. The plaintext goes here and in the Authorization header only; it is never logged. 32+ characters recommended — a shorter one still works but logs a warning at boot.");
			PushServerId = ((BaseUnityPlugin)this).Config.Bind<string>("Push", "PushServerId", "", "The id this server is registered under in the receiver's TRC_SERVERS — must match exactly. 1-32 characters, lowercase a-z, 0-9 and '-' only. No default: pushing stays disabled with a warning at boot until this is set and registered.");
			PushIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Push", "PushIntervalSeconds", 60, "How often (seconds) to attempt a push once something has changed. Clamped 15..3600. The push only runs on the poll tick (StatsPushIntervalSeconds above, default 10s), so the effective cadence rounds up to the next multiple of that — 15 here pushes every 20s, and a StatsPushIntervalSeconds of 120 caps every push at 120s no matter what this is set to. The boot log line reports the effective value actually used.");
			PushHeartbeatMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("Push", "PushHeartbeatMinutes", 10, "How often (minutes), at minimum, to push all three envelopes even when nothing changed, so a receiver that lost its copy (or never had one) recovers and 'last reported' keeps meaning the server is alive. Clamped 1..1440. The effective heartbeat is never shorter than the effective push interval above (a heartbeat only lands on a tick the interval already allows) — raised to match with a boot warning when this value is too small for that.");
			PushClient.Init();
			CensusIntervalMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("Census", "CensusIntervalMinutes", 5, "How often (minutes) to walk every object in the loaded world and count portals, beds, wards, ships, carts, chests and crafting stations for /api/census. Clamped to 1..1440. 0 turns the census off; the endpoint still answers 200 with enabled:false instead of 404.");
			AcceptClientReports = ((BaseUnityPlugin)this).Config.Bind<bool>("Combat", "AcceptClientReports", true, "Accept RavensCall_CombatReport_V1 RPC reports (kills/deaths/damage/fish) from players running the WhereTheCrowFlies client mod. Every report is verified against the connected-player list before anything is credited");
			LogCombatReports = ((BaseUnityPlugin)this).Config.Bind<bool>("Combat", "LogCombatReports", false, "Log every accepted/dropped/rate-limited combat report. Verbose — enable during rollout/verification, then turn back off");
			Log.LogInfo((object)"TheRavensCall 1.7.2 awakens (server-only).");
			try
			{
				_harmony.PatchAll();
				Log.LogInfo((object)"[TheRavensCall] All patches applied.");
			}
			catch (Exception ex)
			{
				Log.LogError((object)("[TheRavensCall] PatchAll failed: " + ex.Message));
			}
			((C