Decompiled source of BeastheimServerMods v1.2.0

plugins/BeastheimServerMods.dll

Decompiled 3 days ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Beastheim.ServerMods.Afk;
using Beastheim.ServerMods.AfkModule;
using Beastheim.ServerMods.CharacterModule;
using Beastheim.ServerMods.Characters.Admission;
using Beastheim.ServerMods.Characters.Backup;
using Beastheim.ServerMods.Characters.Client;
using Beastheim.ServerMods.Characters.Fch;
using Beastheim.ServerMods.Characters.Fresh;
using Beastheim.ServerMods.Characters.Names;
using Beastheim.ServerMods.Characters.Observe;
using Beastheim.ServerMods.Characters.Protocol;
using Beastheim.ServerMods.Characters.Registry;
using Beastheim.ServerMods.Characters.Store;
using Beastheim.ServerMods.Characters.Trace;
using Beastheim.ServerMods.Characters.Transfer;
using Beastheim.ServerMods.Enforcement;
using Beastheim.ServerMods.Enforcement.Client;
using Beastheim.ServerMods.Enforcement.Messaging;
using Beastheim.ServerMods.Enforcement.Protocol;
using Beastheim.ServerMods.Enforcement.Server;
using Beastheim.ServerMods.Enforcement.Util;
using Beastheim.ServerMods.Status;
using Beastheim.ServerMods.WebhookModule;
using Beastheim.ServerMods.Webhooks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Mono.Cecil;
using Mono.Collections.Generic;
using Steamworks;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[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: AssemblyVersion("0.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Beastheim.ServerMods
{
	[BepInPlugin("beastheim.servermods", "Beastheim Server Mods", "1.2.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "beastheim.servermods";

		public const string PluginName = "Beastheim Server Mods";

		public const string PluginVersion = "1.2.0";

		private Harmony _harmony;

		private Harmony _clientHarmony;

		private PolicyWatcher _watcher;

		private ConfigReloader _reloader;

		private CharacterConfig _chars;

		private AfkConfig _afk;

		private Harmony _afkHarmony;

		private bool _dedicated;

		private void Awake()
		{
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0136: Expected O, but got Unknown
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bf: Expected O, but got Unknown
			//IL_0171: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: Expected O, but got Unknown
			_dedicated = Application.isBatchMode;
			EnforcementConfig config = new EnforcementConfig(((BaseUnityPlugin)this).Config);
			string text = Path.Combine(Paths.ConfigPath, "BeastheimServerMods");
			_watcher = new PolicyWatcher(text, () => config.DebounceSeconds, delegate(string m)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)m);
			}, delegate(string m)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)m);
			});
			EnforcementRuntime.Initialize(((BaseUnityPlugin)this).Logger, config, _watcher, "beastheim.servermods");
			_reloader = new ConfigReloader(((BaseUnityPlugin)this).Config, config.Describe, delegate(string m)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)m);
			}, delegate(string m)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)m);
			});
			_reloader.Start();
			_chars = new CharacterConfig(((BaseUnityPlugin)this).Config);
			CharacterServer.Initialize(((BaseUnityPlugin)this).Logger, _chars);
			EnforcementRuntime.ModRejectedHook = delegate(string steam, string codes)
			{
				WebhookRuntime.ModCheckRejected(steam, codes);
			};
			EnforcementRuntime.ConnectionHook = OnConnection;
			_harmony = new Harmony("beastheim.servermods");
			_harmony.PatchAll();
			if (!_dedicated)
			{
				CharacterClient.Install(((BaseUnityPlugin)this).Logger, _chars);
				ResetUi.Initialize(((BaseUnityPlugin)this).Logger, _chars);
				_clientHarmony = new Harmony("beastheim.servermods.ui");
				ResetUiPatches.Apply(_clientHarmony, ((BaseUnityPlugin)this).Logger);
			}
			else
			{
				EnforcementRuntime.EnsureServerReady();
				WebhookRuntime.EnsureInitialised(((BaseUnityPlugin)this).Logger);
				_afk = new AfkConfig(((BaseUnityPlugin)this).Config);
				_afkHarmony = new Harmony("beastheim.servermods.afk");
				AfkRuntime.Initialize(((BaseUnityPlugin)this).Logger, _afk, _afkHarmony);
				if (CharacterServer.Enforced && !CharacterServer.Available)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)"Characters are set to Enforce but the character store is not available: players will be refused.");
				}
			}
			((MonoBehaviour)this).InvokeRepeating("Sweep", 1f, 1f);
			((MonoBehaviour)this).InvokeRepeating("FastTick", 0.2f, 0.2f);
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Beastheim Server Mods 1.2.0 loaded. " + config.Describe() + ". Characters: " + _chars.Mode.Value.ToString() + ". Policy folder: " + text));
		}

		private void Start()
		{
			if (!_dedicated)
			{
				EnforcementRuntime.StartPrewarm();
			}
			try
			{
				if (Chainloader.PluginInfos.ContainsKey("beastheim.characters.observe"))
				{
					string text = "the Observe test build (beastheim.characters.observe) is installed. Remove its plugin folder, its config and its data, then restart. Observe and the authoritative system never run together.";
					if (_dedicated)
					{
						CharacterServer.Disable(text);
					}
					else
					{
						((BaseUnityPlugin)this).Logger.LogError((object)("Beastheim Server Mods: " + text));
					}
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not check for the Observe test build: " + ex.GetType().Name));
			}
		}

		private void OnConnection(ZNet znet, ZNetPeer peer)
		{
			if (znet.IsServer())
			{
				WebhookRuntime.EnsureInitialised(((BaseUnityPlugin)this).Logger);
				CharacterServer.OnNewConnection(znet, peer);
			}
			else
			{
				CharacterClient.OnNewConnection(peer.m_rpc);
			}
		}

		private void Sweep()
		{
			EnforcementRuntime.Sweep();
			if (_dedicated)
			{
				AfkRuntime.Tick();
			}
			WebhookRuntime.Tick();
		}

		private void FastTick()
		{
			CharacterServer.Tick();
		}

		private void Update()
		{
			if (!_dedicated)
			{
				CharacterClient.Tick();
				StatusPoller.Update();
				ResetUi.Update();
			}
		}

		private void OnDestroy()
		{
			CharacterServer.Shutdown();
			CharacterClient.Shutdown();
			if (_reloader != null)
			{
				_reloader.Dispose();
			}
			if (_watcher != null)
			{
				_watcher.Dispose();
			}
			if (_harmony != null)
			{
				_harmony.UnpatchSelf();
			}
			if (_clientHarmony != null)
			{
				_clientHarmony.UnpatchSelf();
			}
			if (_afkHarmony != null)
			{
				_afkHarmony.UnpatchSelf();
			}
		}
	}
	internal static class StatusPoller
	{
		public unsafe static void Update()
		{
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Invalid comparison between Unknown and I4
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Invalid comparison between Unknown and I4
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Invalid comparison between Unknown and I4
			try
			{
				BsmStatusTracker tracker = BsmStatus.Tracker;
				ZNet instance = ZNet.instance;
				if (!((Object)(object)instance != (Object)null) || !instance.IsServer())
				{
					ConnectionStatus connectionStatus = ZNet.GetConnectionStatus();
					bool loading = tracker.Current.Loading;
					if ((int)connectionStatus == 1 && !loading)
					{
						tracker.ConnectStarted();
					}
					else if ((int)connectionStatus == 2)
					{
						tracker.VanillaConnected(connected: true);
					}
					else if ((int)connectionStatus >= 3 && loading)
					{
						tracker.Disconnected(((object)(*(ConnectionStatus*)(&connectionStatus))/*cast due to .constrained prefix*/).ToString());
					}
					if ((Object)(object)Player.m_localPlayer != (Object)null)
					{
						tracker.PlayerSpawned();
					}
					else if ((Object)(object)instance == (Object)null && (tracker.Current.State == BsmState.InWorld || tracker.Current.State == BsmState.EnteringWorld))
					{
						tracker.Disconnected("");
					}
					tracker.Tick();
				}
			}
			catch (Exception)
			{
			}
		}
	}
	public static class BsmAfk
	{
		public static bool Suppress(string steam64, string reason, double maxSeconds)
		{
			try
			{
				return AfkRuntime.Suppress(steam64, reason, maxSeconds);
			}
			catch
			{
				return false;
			}
		}

		public static void Release(string steam64)
		{
			try
			{
				AfkRuntime.Release(steam64);
			}
			catch
			{
			}
		}
	}
	public static class BsmIdentity
	{
		public const int ApiVersion = 1;

		public static bool TryGetCharacterId(object connection, out long characterId)
		{
			characterId = 0L;
			try
			{
				ZRpc val = (ZRpc)((connection is ZRpc) ? connection : null);
				return val != null && CharacterServer.TryGetVerifiedCharacter(val, out characterId);
			}
			catch
			{
				characterId = 0L;
				return false;
			}
		}

		public static bool TryGetCharacterIdBySteam64(string steam64, out long characterId)
		{
			characterId = 0L;
			try
			{
				return CharacterServer.TryGetVerifiedCharacterBySteam(steam64, out characterId);
			}
			catch
			{
				characterId = 0L;
				return false;
			}
		}
	}
}
namespace Beastheim.ServerMods.Afk
{
	public sealed class AfkSettings
	{
		public bool Enabled;

		public double TimeoutSeconds = 1800.0;

		public double[] WarningSecondsRemaining = new double[2] { 300.0, 60.0 };

		public double RadiusMeters = 3.0;

		public double GraceSeconds = 60.0;

		public double SleepMaxSeconds = 1200.0;

		public double FinalizeWaitSeconds = 15.0;

		public double FinalizeSafetyMarginSeconds = 5.0;

		public double DrainSeconds = 3.0;

		public double UnconfirmedGraceSeconds = 3.0;

		public double SuppressCapSeconds = 600.0;

		public bool ExemptAdmins = true;

		public HashSet<string> ExemptSteamIds = new HashSet<string>(StringComparer.Ordinal);

		public bool CountActionSignals = true;

		public bool DeadCountsAsIdle = true;

		public string WarningText = "You appear to be away. You will be disconnected in {minutes} minute(s) unless you move or act.";

		public string KickTextSaved = "You were disconnected for inactivity. Your character was saved. You can rejoin at any time.";

		public string KickTextUnconfirmed = "You were disconnected for inactivity. The server could not confirm that your latest progress was saved, so recent changes may be lost. You can rejoin at any time.";

		public AfkSettings Validated()
		{
			AfkSettings s = (AfkSettings)MemberwiseClone();
			s.TimeoutSeconds = Math.Max(60.0, s.TimeoutSeconds);
			s.RadiusMeters = Math.Max(0.25, s.RadiusMeters);
			s.GraceSeconds = Math.Max(0.0, s.GraceSeconds);
			s.SleepMaxSeconds = Math.Max(0.0, s.SleepMaxSeconds);
			s.FinalizeWaitSeconds = Math.Max(2.0, s.FinalizeWaitSeconds);
			s.FinalizeSafetyMarginSeconds = Math.Max(1.0, s.FinalizeSafetyMarginSeconds);
			s.DrainSeconds = Math.Max(0.0, s.DrainSeconds);
			s.UnconfirmedGraceSeconds = Math.Max(0.0, s.UnconfirmedGraceSeconds);
			s.SuppressCapSeconds = Math.Max(1.0, s.SuppressCapSeconds);
			s.WarningSecondsRemaining = (from w in (s.WarningSecondsRemaining ?? new double[0]).Where((double w) => w > 0.0 && w < s.TimeoutSeconds).Distinct()
				orderby w descending
				select w).ToArray();
			s.ExemptSteamIds = new HashSet<string>(s.ExemptSteamIds ?? new HashSet<string>(), StringComparer.Ordinal);
			s.WarningText = (string.IsNullOrWhiteSpace(s.WarningText) ? new AfkSettings().WarningText : s.WarningText);
			s.KickTextSaved = (string.IsNullOrWhiteSpace(s.KickTextSaved) ? new AfkSettings().KickTextSaved : s.KickTextSaved);
			s.KickTextUnconfirmed = (string.IsNullOrWhiteSpace(s.KickTextUnconfirmed) ? new AfkSettings().KickTextUnconfirmed : s.KickTextUnconfirmed);
			return s;
		}

		public static double[] ParseWarningMinutes(string text)
		{
			List<double> list = new List<double>();
			string[] array = (text ?? "").Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
			for (int i = 0; i < array.Length; i++)
			{
				if (double.TryParse(array[i], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && result > 0.0 && result < 1440.0)
				{
					list.Add(result * 60.0);
				}
			}
			return list.ToArray();
		}

		public static HashSet<string> ParseSteamIds(string text)
		{
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			string[] array = (text ?? "").Split(new char[6] { ',', ';', ' ', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries);
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length >= 5 && text2.Length <= 24 && text2.All(char.IsDigit))
				{
					hashSet.Add(text2);
				}
			}
			return hashSet;
		}
	}
	public struct AfkPeer
	{
		public object Key;

		public string Steam64;

		public string Name;

		public bool Ready;

		public bool Busy;

		public bool Admin;

		public bool CanFinalizeCharacter;

		public double X;

		public double Z;

		public bool InBed;

		public bool Dead;

		public long ActionCounter;
	}
	public enum AfkActionType
	{
		Warn,
		BeginFinalize,
		RequestVanillaSave,
		Notice,
		Disconnect
	}
	public sealed class AfkAction
	{
		public AfkActionType Type;

		public object Key;

		public string Steam64 = "";

		public string Name = "";

		public string Text = "";

		public double Seconds;

		public bool Saved;
	}
	public enum FinalizeOutcome
	{
		Saved,
		TimedOut,
		Unavailable,
		Disconnected
	}
	public enum AfkEventType
	{
		Warned,
		WarningCancelled,
		FinalizeStarted,
		FinalizeResult,
		Kicked,
		KickedUnconfirmed,
		SuppressionExpired
	}
	public sealed class AfkEvent
	{
		public AfkEventType Type;

		public string Steam64 = "";

		public string Name = "";

		public string Detail = "";

		public double IdleSeconds;
	}
	public sealed class AfkEngine
	{
		private enum Phase
		{
			Present,
			Finalizing,
			Ending
		}

		private sealed class State
		{
			public Phase Phase;

			public string Steam = "";

			public string Name = "";

			public double ReadyAt = -1.0;

			public double LastPresence;

			public double AnchorX;

			public double AnchorZ;

			public double SleepStart = -1.0;

			public double SuppressUntil = -1.0;

			public double FinalizeDeadline;

			public double EndAt;

			public long LastAction;

			public bool HaveAction;

			public bool ChatFlag;

			public bool DisconnectSent;

			public bool UnconfirmedPath;

			public int WarnedMask;

			public string SuppressReason = "";
		}

		private AfkSettings _s = new AfkSettings().Validated();

		private readonly Dictionary<object, State> _states = new Dictionary<object, State>();

		private readonly Dictionary<string, KeyValuePair<double, string>> _pendingSuppress = new Dictionary<string, KeyValuePair<double, string>>(StringComparer.Ordinal);

		private readonly List<AfkEvent> _events = new List<AfkEvent>();

		public AfkSettings Settings
		{
			get
			{
				return _s;
			}
			set
			{
				_s = (value ?? new AfkSettings()).Validated();
				if (!_s.Enabled)
				{
					_states.Clear();
					_pendingSuppress.Clear();
				}
			}
		}

		public int TrackedCount => _states.Count;

		public List<AfkEvent> DrainEvents()
		{
			List<AfkEvent> result = new List<AfkEvent>(_events);
			_events.Clear();
			return result;
		}

		public List<AfkAction> Observe(IEnumerable<AfkPeer> peers, double now)
		{
			List<AfkAction> list = new List<AfkAction>();
			if (!_s.Enabled)
			{
				if (_states.Count > 0)
				{
					_states.Clear();
				}
				return list;
			}
			HashSet<object> seen = new HashSet<object>();
			foreach (AfkPeer peer in peers)
			{
				if (peer.Key != null)
				{
					seen.Add(peer.Key);
					if (!_states.TryGetValue(peer.Key, out var value))
					{
						value = new State
						{
							LastPresence = now
						};
						_states[peer.Key] = value;
					}
					value.Steam = peer.Steam64 ?? "";
					value.Name = peer.Name ?? "";
					try
					{
						Step(value, peer, now, list);
					}
					catch (Exception)
					{
						_states.Remove(peer.Key);
					}
				}
			}
			foreach (object item in _states.Keys.Where((object k) => !seen.Contains(k)).ToList())
			{
				_states.Remove(item);
			}
			return list;
		}

		public void NoteChat(object key, double now)
		{
			if (_s.Enabled && key != null && _states.TryGetValue(key, out var value) && value.Phase == Phase.Present)
			{
				value.ChatFlag = true;
			}
		}

		public List<AfkAction> OnFinalizeResult(object key, FinalizeOutcome outcome, double now)
		{
			List<AfkAction> list = new List<AfkAction>();
			if (_s.Enabled && key != null && _states.TryGetValue(key, out var value) && value.Phase == Phase.Finalizing)
			{
				Conclude(key, value, outcome, now, list);
			}
			return list;
		}

		public bool Suppress(string steam64, string reason, double maxSeconds, double now)
		{
			if (!_s.Enabled || string.IsNullOrEmpty(steam64) || !(maxSeconds > 0.0))
			{
				return false;
			}
			double num = now + Math.Min(maxSeconds, _s.SuppressCapSeconds);
			string text = reason ?? "";
			bool flag = false;
			foreach (State value in _states.Values)
			{
				if (value.Steam == steam64 && value.Phase == Phase.Present)
				{
					value.SuppressUntil = num;
					value.SuppressReason = text;
					flag = true;
				}
			}
			if (!flag)
			{
				_pendingSuppress[steam64] = new KeyValuePair<double, string>(num, text);
			}
			return true;
		}

		public void Release(string steam64, double now)
		{
			if (string.IsNullOrEmpty(steam64))
			{
				return;
			}
			_pendingSuppress.Remove(steam64);
			foreach (State value in _states.Values)
			{
				if (value.Steam == steam64 && value.SuppressUntil >= 0.0)
				{
					value.SuppressUntil = -1.0;
					value.LastPresence = now;
					value.WarnedMask = 0;
				}
			}
		}

		private void Step(State st, AfkPeer p, double now, List<AfkAction> actions)
		{
			if (st.Phase == Phase.Ending)
			{
				if (!st.DisconnectSent && now >= st.EndAt)
				{
					st.DisconnectSent = true;
					actions.Add(new AfkAction
					{
						Type = AfkActionType.Disconnect,
						Key = p.Key,
						Steam64 = st.Steam,
						Name = st.Name
					});
				}
				return;
			}
			if (st.Phase == Phase.Finalizing)
			{
				if (now >= st.FinalizeDeadline)
				{
					Conclude(p.Key, st, (!st.UnconfirmedPath) ? FinalizeOutcome.TimedOut : FinalizeOutcome.Unavailable, now, actions);
				}
				return;
			}
			if (!p.Ready || p.Busy)
			{
				st.ReadyAt = -1.0;
				Present(st, p, now, _: false);
				return;
			}
			if (st.ReadyAt < 0.0)
			{
				st.ReadyAt = now;
				Present(st, p, now, _: false);
				return;
			}
			if (now < st.ReadyAt + _s.GraceSeconds)
			{
				Present(st, p, now, _: false);
				return;
			}
			if ((_s.ExemptAdmins && p.Admin) || _s.ExemptSteamIds.Contains(p.Steam64 ?? ""))
			{
				Present(st, p, now, _: false);
				return;
			}
			if (st.SuppressUntil < 0.0 && !string.IsNullOrEmpty(p.Steam64) && _pendingSuppress.TryGetValue(p.Steam64, out var value))
			{
				st.SuppressUntil = value.Key;
				st.SuppressReason = value.Value;
				_pendingSuppress.Remove(p.Steam64);
			}
			if (st.SuppressUntil >= 0.0)
			{
				if (now < st.SuppressUntil)
				{
					Present(st, p, now, _: false);
					return;
				}
				_events.Add(new AfkEvent
				{
					Type = AfkEventType.SuppressionExpired,
					Steam64 = st.Steam,
					Name = st.Name,
					Detail = st.SuppressReason
				});
				st.SuppressUntil = -1.0;
				Present(st, p, now, _: false);
				return;
			}
			bool flag = false;
			double num = p.X - st.AnchorX;
			double num2 = p.Z - st.AnchorZ;
			if (num * num + num2 * num2 > _s.RadiusMeters * _s.RadiusMeters)
			{
				flag = true;
				st.AnchorX = p.X;
				st.AnchorZ = p.Z;
			}
			if (_s.CountActionSignals)
			{
				if (st.HaveAction && p.ActionCounter != st.LastAction)
				{
					flag = true;
				}
				st.LastAction = p.ActionCounter;
				st.HaveAction = true;
			}
			if (st.ChatFlag)
			{
				flag = true;
				st.ChatFlag = false;
			}
			if (p.InBed)
			{
				if (st.SleepStart < 0.0)
				{
					st.SleepStart = now;
				}
				if (now - st.SleepStart < _s.SleepMaxSeconds)
				{
					flag = true;
				}
			}
			else
			{
				st.SleepStart = -1.0;
			}
			if (p.Dead && !_s.DeadCountsAsIdle)
			{
				flag = true;
			}
			if (flag)
			{
				if (st.WarnedMask != 0)
				{
					_events.Add(new AfkEvent
					{
						Type = AfkEventType.WarningCancelled,
						Steam64 = st.Steam,
						Name = st.Name,
						IdleSeconds = now - st.LastPresence
					});
				}
				st.WarnedMask = 0;
				st.LastPresence = now;
				return;
			}
			double num3 = now - st.LastPresence;
			double[] warningSecondsRemaining = _s.WarningSecondsRemaining;
			int num4 = -1;
			for (int i = 0; i < warningSecondsRemaining.Length; i++)
			{
				if (num3 >= _s.TimeoutSeconds - warningSecondsRemaining[i] && (st.WarnedMask & (1 << i)) == 0)
				{
					st.WarnedMask |= 1 << i;
					num4 = i;
				}
			}
			if (num4 >= 0 && num3 < _s.TimeoutSeconds)
			{
				double num5 = warningSecondsRemaining[num4];
				string text = _s.WarningText.Replace("{minutes}", Math.Ceiling(num5 / 60.0).ToString(CultureInfo.InvariantCulture)).Replace("{seconds}", ((long)Math.Round(num5)).ToString(CultureInfo.InvariantCulture));
				actions.Add(new AfkAction
				{
					Type = AfkActionType.Warn,
					Key = p.Key,
					Steam64 = st.Steam,
					Name = st.Name,
					Text = text,
					Seconds = num5
				});
				_events.Add(new AfkEvent
				{
					Type = AfkEventType.Warned,
					Steam64 = st.Steam,
					Name = st.Name,
					IdleSeconds = num3,
					Detail = num5.ToString("0", CultureInfo.InvariantCulture) + " s remaining"
				});
			}
			if (num3 >= _s.TimeoutSeconds)
			{
				st.Phase = Phase.Finalizing;
				_events.Add(new AfkEvent
				{
					Type = AfkEventType.FinalizeStarted,
					Steam64 = st.Steam,
					Name = st.Name,
					IdleSeconds = num3,
					Detail = (p.CanFinalizeCharacter ? "character finalization" : "vanilla save request only (character module not finalizing)")
				});
				if (p.CanFinalizeCharacter)
				{
					st.UnconfirmedPath = false;
					st.FinalizeDeadline = now + _s.FinalizeWaitSeconds + _s.FinalizeSafetyMarginSeconds;
					actions.Add(new AfkAction
					{
						Type = AfkActionType.BeginFinalize,
						Key = p.Key,
						Steam64 = st.Steam,
						Name = st.Name,
						Seconds = _s.FinalizeWaitSeconds
					});
				}
				else
				{
					st.UnconfirmedPath = true;
					st.FinalizeDeadline = now + _s.UnconfirmedGraceSeconds;
					actions.Add(new AfkAction
					{
						Type = AfkActionType.RequestVanillaSave,
						Key = p.Key,
						Steam64 = st.Steam,
						Name = st.Name
					});
				}
			}
		}

		private void Present(State st, AfkPeer p, double now, bool _)
		{
			st.LastPresence = now;
			st.AnchorX = p.X;
			st.AnchorZ = p.Z;
			st.WarnedMask = 0;
			st.ChatFlag = false;
			st.LastAction = p.ActionCounter;
			st.HaveAction = true;
			if (!p.InBed)
			{
				st.SleepStart = -1.0;
			}
		}

		private void Conclude(object key, State st, FinalizeOutcome outcome, double now, List<AfkAction> actions)
		{
			_events.Add(new AfkEvent
			{
				Type = AfkEventType.FinalizeResult,
				Steam64 = st.Steam,
				Name = st.Name,
				Detail = outcome.ToString()
			});
			if (outcome == FinalizeOutcome.Disconnected)
			{
				_states.Remove(key);
				return;
			}
			bool flag = outcome == FinalizeOutcome.Saved;
			st.Phase = Phase.Ending;
			st.EndAt = now + _s.DrainSeconds;
			actions.Add(new AfkAction
			{
				Type = AfkActionType.Notice,
				Key = key,
				Steam64 = st.Steam,
				Name = st.Name,
				Text = (flag ? _s.KickTextSaved : _s.KickTextUnconfirmed),
				Saved = flag
			});
			_events.Add(new AfkEvent
			{
				Type = (flag ? AfkEventType.Kicked : AfkEventType.KickedUnconfirmed),
				Steam64 = st.Steam,
				Name = st.Name,
				Detail = outcome.ToString()
			});
		}
	}
}
namespace Beastheim.ServerMods.Status
{
	public enum BsmState
	{
		Idle,
		Connecting,
		CheckingMods,
		ValidatingCharacter,
		WaitingForServer,
		SynchronizingWorld,
		EnteringWorld,
		InWorld,
		Rejected,
		Failed
	}
	public sealed class BsmStatusSnapshot
	{
		public BsmState State;

		public string Name = "";

		public string Detail = "";

		public string RejectionCode = "";

		public string RejectionText = "";

		public int Sequence;

		public double EnteredAt;

		public int StageIndex;

		public int StageCount;

		public bool Loading;

		public int ModsAccepted;

		public int CharacterAccepted;

		public bool CharacterCheckExpected;
	}
	public sealed class BsmStatusTracker
	{
		public const double EnteringHoldSeconds = 2.0;

		public const int Stages = 6;

		private BsmStatusSnapshot _s = New(BsmState.Idle, 0, 0.0);

		private readonly Func<double> _clock;

		private bool _modsOk;

		private bool _modsKnown;

		private bool _charOk;

		private bool _charKnown;

		private bool _charExpected;

		private bool _vanillaConnected;

		private bool _spawned;

		public readonly List<string> History = new List<string>();

		public BsmStatusSnapshot Current => _s;

		private bool Active
		{
			get
			{
				if (_s.State != BsmState.Idle && _s.State != BsmState.InWorld && _s.State != BsmState.Rejected)
				{
					return _s.State != BsmState.Failed;
				}
				return false;
			}
		}

		public event Action<BsmStatusSnapshot> Changed;

		public BsmStatusTracker(Func<double> clock = null)
		{
			Stopwatch sw = Stopwatch.StartNew();
			_clock = clock ?? ((Func<double>)(() => sw.Elapsed.TotalSeconds));
		}

		public void ConnectStarted()
		{
			_modsOk = (_modsKnown = (_charOk = (_charKnown = (_vanillaConnected = (_spawned = false)))));
			_charExpected = false;
			Set(BsmState.Connecting, "Connecting to the server.");
		}

		public void ModListSending()
		{
			if (!Active)
			{
				ConnectStarted();
			}
			Set(BsmState.CheckingMods, "Checking your mods against the server's list.");
		}

		public void ModDecision(bool accepted, string code, string safeText)
		{
			if (Active)
			{
				_modsKnown = true;
				_modsOk = accepted;
				if (!accepted)
				{
					Reject(code, safeText);
				}
				else
				{
					Advance();
				}
			}
		}

		public void CharacterCheckExpected()
		{
			if (Active)
			{
				_charExpected = true;
			}
		}

		public void CharacterClaimSent()
		{
			if (Active && _modsKnown && _modsOk)
			{
				Set(BsmState.ValidatingCharacter, "Verifying your selected character.");
			}
			_charExpected = true;
		}

		public void CharacterDecision(bool accepted, string code, string safeText)
		{
			if (Active)
			{
				_charKnown = true;
				_charOk = accepted;
				if (!accepted)
				{
					Reject(code, safeText);
				}
				else
				{
					Advance();
				}
			}
		}

		public void VanillaConnected(bool connected)
		{
			if (Active && _vanillaConnected != connected)
			{
				_vanillaConnected = connected;
				if (connected)
				{
					Advance();
				}
			}
		}

		public void PlayerSpawned()
		{
			if (_s.State != BsmState.Idle && _s.State != BsmState.InWorld && _s.State != BsmState.Rejected)
			{
				_spawned = true;
				Set(BsmState.EnteringWorld, "Entering Beastheim.");
			}
		}

		public void Disconnected(string reason)
		{
			switch (_s.State)
			{
			case BsmState.Idle:
			case BsmState.Rejected:
			case BsmState.Failed:
				break;
			case BsmState.InWorld:
				Set(BsmState.Idle, "");
				break;
			default:
				Fail("C.Disconnected", string.IsNullOrEmpty(reason) ? "The connection to the server ended." : reason);
				break;
			}
		}

		public void Reset()
		{
			_modsOk = (_modsKnown = (_charOk = (_charKnown = (_charExpected = (_vanillaConnected = (_spawned = false))))));
			Set(BsmState.Idle, "");
		}

		public void Tick()
		{
			if (_s.State == BsmState.EnteringWorld && _clock() - _s.EnteredAt >= 2.0)
			{
				Set(BsmState.InWorld, "");
			}
		}

		private void Advance()
		{
			if (Active)
			{
				BsmState bsmState = (_spawned ? BsmState.EnteringWorld : ((_vanillaConnected && (_modsOk || !_modsKnown) && (_charOk || !_charExpected)) ? BsmState.SynchronizingWorld : ((_modsKnown && _modsOk && (!_charExpected || _charKnown) && (!_charExpected || _charOk)) ? BsmState.WaitingForServer : ((_modsKnown && _modsOk && _charExpected) ? BsmState.ValidatingCharacter : ((!_modsKnown || !_modsOk) ? _s.State : BsmState.WaitingForServer)))));
				if (bsmState > _s.State)
				{
					Set(bsmState, DetailFor(bsmState));
				}
			}
		}

		private void Reject(string code, string text)
		{
			BsmStatusSnapshot bsmStatusSnapshot = New(BsmState.Rejected, _s.Sequence + 1, _clock());
			bsmStatusSnapshot.RejectionCode = code ?? "";
			bsmStatusSnapshot.RejectionText = text ?? "";
			bsmStatusSnapshot.Detail = "The server refused this connection.";
			bsmStatusSnapshot.ModsAccepted = (_modsKnown ? (_modsOk ? 1 : 2) : 0);
			bsmStatusSnapshot.CharacterAccepted = (_charKnown ? (_charOk ? 1 : 2) : 0);
			bsmStatusSnapshot.CharacterCheckExpected = _charExpected;
			Publish(bsmStatusSnapshot);
		}

		private void Fail(string code, string text)
		{
			BsmStatusSnapshot bsmStatusSnapshot = New(BsmState.Failed, _s.Sequence + 1, _clock());
			bsmStatusSnapshot.RejectionCode = code ?? "";
			bsmStatusSnapshot.RejectionText = text ?? "";
			bsmStatusSnapshot.Detail = "The connection did not complete.";
			bsmStatusSnapshot.ModsAccepted = (_modsKnown ? (_modsOk ? 1 : 2) : 0);
			bsmStatusSnapshot.CharacterAccepted = (_charKnown ? (_charOk ? 1 : 2) : 0);
			bsmStatusSnapshot.CharacterCheckExpected = _charExpected;
			Publish(bsmStatusSnapshot);
		}

		private void Set(BsmState state, string detail)
		{
			if (state != _s.State || !(detail == _s.Detail))
			{
				BsmStatusSnapshot bsmStatusSnapshot = New(state, _s.Sequence + 1, _clock());
				bsmStatusSnapshot.Detail = detail ?? "";
				bsmStatusSnapshot.ModsAccepted = (_modsKnown ? (_modsOk ? 1 : 2) : 0);
				bsmStatusSnapshot.CharacterAccepted = (_charKnown ? (_charOk ? 1 : 2) : 0);
				bsmStatusSnapshot.CharacterCheckExpected = _charExpected;
				Publish(bsmStatusSnapshot);
			}
		}

		private void Publish(BsmStatusSnapshot s)
		{
			_s = s;
			History.Add(s.EnteredAt.ToString("0.0", CultureInfo.InvariantCulture) + "s " + s.State.ToString() + ((s.RejectionCode.Length > 0) ? (" [" + s.RejectionCode + "]") : ""));
			if (History.Count > 40)
			{
				History.RemoveAt(0);
			}
			Action<BsmStatusSnapshot> action = this.Changed;
			if (action != null)
			{
				try
				{
					action(s);
				}
				catch
				{
				}
			}
		}

		private static string DetailFor(BsmState s)
		{
			return s switch
			{
				BsmState.Connecting => "Connecting to the server.", 
				BsmState.CheckingMods => "Checking your mods against the server's list.", 
				BsmState.ValidatingCharacter => "Verifying your selected character.", 
				BsmState.WaitingForServer => "Waiting for the server to finish letting you in.", 
				BsmState.SynchronizingWorld => "Loading the world.", 
				BsmState.EnteringWorld => "Entering Beastheim.", 
				_ => "", 
			};
		}

		private static BsmStatusSnapshot New(BsmState state, int seq, double now)
		{
			BsmStatusSnapshot bsmStatusSnapshot = new BsmStatusSnapshot
			{
				State = state,
				Sequence = seq,
				EnteredAt = now,
				StageCount = 6
			};
			switch (state)
			{
			case BsmState.Connecting:
				bsmStatusSnapshot.Name = "CONNECTING";
				bsmStatusSnapshot.StageIndex = 1;
				bsmStatusSnapshot.Loading = true;
				break;
			case BsmState.CheckingMods:
				bsmStatusSnapshot.Name = "CHECKING MODS";
				bsmStatusSnapshot.StageIndex = 2;
				bsmStatusSnapshot.Loading = true;
				break;
			case BsmState.ValidatingCharacter:
				bsmStatusSnapshot.Name = "VALIDATING CHARACTER";
				bsmStatusSnapshot.StageIndex = 3;
				bsmStatusSnapshot.Loading = true;
				break;
			case BsmState.WaitingForServer:
				bsmStatusSnapshot.Name = "WAITING FOR THE SERVER";
				bsmStatusSnapshot.StageIndex = 4;
				bsmStatusSnapshot.Loading = true;
				break;
			case BsmState.SynchronizingWorld:
				bsmStatusSnapshot.Name = "SYNCHRONIZING WORLD";
				bsmStatusSnapshot.StageIndex = 5;
				bsmStatusSnapshot.Loading = true;
				break;
			case BsmState.EnteringWorld:
				bsmStatusSnapshot.Name = "ENTERING BEASTHEIM";
				bsmStatusSnapshot.StageIndex = 6;
				bsmStatusSnapshot.Loading = true;
				break;
			case BsmState.InWorld:
				bsmStatusSnapshot.Name = "IN BEASTHEIM";
				bsmStatusSnapshot.StageIndex = 6;
				bsmStatusSnapshot.Loading = false;
				break;
			case BsmState.Rejected:
				bsmStatusSnapshot.Name = "CONNECTION REFUSED";
				bsmStatusSnapshot.StageIndex = 0;
				bsmStatusSnapshot.Loading = false;
				break;
			case BsmState.Failed:
				bsmStatusSnapshot.Name = "CONNECTION FAILED";
				bsmStatusSnapshot.StageIndex = 0;
				bsmStatusSnapshot.Loading = false;
				break;
			default:
				bsmStatusSnapshot.Name = "IDLE";
				bsmStatusSnapshot.StageIndex = 0;
				bsmStatusSnapshot.Loading = false;
				break;
			}
			return bsmStatusSnapshot;
		}
	}
	public static class BsmStatus
	{
		public static readonly BsmStatusTracker Tracker;

		public const string ApiVersion = "1";

		public static int State => (int)Tracker.Current.State;

		public static string StateName => Tracker.Current.Name;

		public static string Detail => Tracker.Current.Detail;

		public static bool IsLoading => Tracker.Current.Loading;

		public static int Sequence => Tracker.Current.Sequence;

		public static int StageIndex => Tracker.Current.StageIndex;

		public static int StageCount => Tracker.Current.StageCount;

		public static string RejectionCode => Tracker.Current.RejectionCode;

		public static string RejectionText => Tracker.Current.RejectionText;

		public static bool IsRejected
		{
			get
			{
				if (Tracker.Current.State != BsmState.Rejected)
				{
					return Tracker.Current.State == BsmState.Failed;
				}
				return true;
			}
		}

		public static int ModsAccepted => Tracker.Current.ModsAccepted;

		public static int CharacterAccepted => Tracker.Current.CharacterAccepted;

		public static event Action<string> StateChanged;

		internal static void Raise(string name)
		{
			Action<string> stateChanged = BsmStatus.StateChanged;
			if (stateChanged != null)
			{
				try
				{
					stateChanged(name);
				}
				catch
				{
				}
			}
		}

		static BsmStatus()
		{
			Tracker = new BsmStatusTracker();
			Tracker.Changed += delegate(BsmStatusSnapshot s)
			{
				Raise(s.Name);
			};
		}
	}
}
namespace Beastheim.ServerMods.Webhooks
{
	public sealed class HttpClientPoster : IHttpPoster
	{
		private static readonly HttpClient Client = Create();

		private static HttpClient Create()
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Expected O, but got Unknown
			try
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			}
			catch
			{
			}
			HttpClient val = new HttpClient
			{
				Timeout = TimeSpan.FromSeconds(30.0)
			};
			val.DefaultRequestHeaders.UserAgent.ParseAdd("BeastheimServerMods/1.2");
			return val;
		}

		public PostResult Post(string url, string json, int timeoutSeconds)
		{
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0025: Expected O, but got Unknown
			try
			{
				using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, timeoutSeconds)));
				StringContent val = new StringContent(json, (Encoding)new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), "application/json");
				try
				{
					HttpResponseMessage result = Client.PostAsync(url, (HttpContent)(object)val, cancellationTokenSource.Token).GetAwaiter().GetResult();
					try
					{
						PostResult postResult = new PostResult
						{
							Status = (int)result.StatusCode
						};
						if (!result.IsSuccessStatusCode)
						{
							postResult.RetryAfterSeconds = RetryAfter(result);
						}
						return postResult;
					}
					finally
					{
						((IDisposable)result)?.Dispose();
					}
				}
				finally
				{
					((IDisposable)val)?.Dispose();
				}
			}
			catch (OperationCanceledException)
			{
				return new PostResult
				{
					Status = 0,
					Error = "Timeout"
				};
			}
			catch (Exception ex2)
			{
				Exception ex3 = ((ex2 is AggregateException && ((AggregateException)ex2).InnerException != null) ? ((AggregateException)ex2).InnerException : ex2);
				return new PostResult
				{
					Status = 0,
					Error = ex3.GetType().Name
				};
			}
		}

		private static double RetryAfter(HttpResponseMessage response)
		{
			try
			{
				if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta.HasValue)
				{
					return response.Headers.RetryAfter.Delta.Value.TotalSeconds;
				}
				IEnumerable<string> source = default(IEnumerable<string>);
				if (((HttpHeaders)response.Headers).TryGetValues("Retry-After", ref source) && double.TryParse(source.FirstOrDefault(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					return result;
				}
				if (((HttpHeaders)response.Headers).TryGetValues("X-RateLimit-Reset-After", ref source) && double.TryParse(source.FirstOrDefault(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result2))
				{
					return result2;
				}
			}
			catch
			{
			}
			return 0.0;
		}
	}
	public sealed class PostResult
	{
		public int Status;

		public double RetryAfterSeconds;

		public string Error = "";

		public bool Ok
		{
			get
			{
				if (Status >= 200)
				{
					return Status < 300;
				}
				return false;
			}
		}
	}
	public interface IHttpPoster
	{
		PostResult Post(string url, string json, int timeoutSeconds);
	}
	public sealed class WebhookDispatcher
	{
		private sealed class Attempt
		{
			public WebhookMessage Message;

			public string Json;

			public int Attempts;
		}

		private sealed class Route
		{
			public string Name;

			public Func<string> Url;

			public bool Configured;

			public readonly LinkedList<WebhookEvent> Queue = new LinkedList<WebhookEvent>();

			public double Tokens;

			public double LastRefill;

			public double NextAttemptAt;

			public int ConsecutiveFailures;

			public double CircuitOpenUntil;

			public string DisabledReason = "";

			public Attempt Current;

			public bool LoggedPaused;
		}

		private readonly WebhookSettings _s;

		private readonly IHttpPoster _http;

		private readonly Action<string> _log;

		private readonly object _gate = new object();

		private readonly Route _public;

		private readonly Route _admin;

		private bool _initialised;

		public long Sent;

		public long Dropped;

		public long Failed;

		public long Merged;

		public long Refused;

		private int _active;

		public int QueueDepth
		{
			get
			{
				lock (_gate)
				{
					return _public.Queue.Count + _admin.Queue.Count;
				}
			}
		}

		public bool Busy => Volatile.Read(in _active) > 0;

		public bool Enabled
		{
			get
			{
				if (!_public.Configured)
				{
					return _admin.Configured;
				}
				return true;
			}
		}

		public WebhookDispatcher(WebhookSettings settings, IHttpPoster http, Action<string> log)
		{
			_s = settings;
			_http = http;
			_log = log ?? ((Action<string>)delegate
			{
			});
			_public = new Route
			{
				Name = "public",
				Url = () => _s.PublicUrl.Trim(),
				Configured = settings.PublicConfigured
			};
			_admin = new Route
			{
				Name = "admin",
				Url = () => _s.AdminUrl.Trim(),
				Configured = settings.AdminConfigured
			};
		}

		public bool Publish(WebhookEvent e)
		{
			if (e == null || !Enabled)
			{
				return false;
			}
			bool flag = false;
			lock (_gate)
			{
				if (_public.Configured && _s.PublicEvents.Contains(e.Type))
				{
					flag |= Enqueue(_public, e);
				}
				if (_admin.Configured && _s.AdminEvents.Contains(e.Type))
				{
					flag |= Enqueue(_admin, e);
				}
			}
			return flag;
		}

		private bool Enqueue(Route r, WebhookEvent e)
		{
			if (r.DisabledReason.Length > 0)
			{
				Refused++;
				return false;
			}
			bool result = true;
			while (r.Queue.Count >= Math.Max(1, _s.MaxQueue))
			{
				r.Queue.RemoveFirst();
				Dropped++;
				result = false;
			}
			r.Queue.AddLast(Clone(e));
			return result;
		}

		private static WebhookEvent Clone(WebhookEvent e)
		{
			return new WebhookEvent
			{
				Type = e.Type,
				Utc = e.Utc,
				PlayerName = (e.PlayerName ?? ""),
				Steam64 = (e.Steam64 ?? ""),
				Code = (e.Code ?? ""),
				Detail = (e.Detail ?? ""),
				Count = Math.Max(1, e.Count),
				Actor = (e.Actor ?? "")
			};
		}

		public int Step(double now)
		{
			Interlocked.Increment(ref _active);
			try
			{
				return StepCore(now);
			}
			finally
			{
				Interlocked.Decrement(ref _active);
			}
		}

		private int StepCore(double now)
		{
			if (!_initialised)
			{
				_initialised = true;
				_public.LastRefill = (_admin.LastRefill = now);
				_public.Tokens = (_admin.Tokens = _s.BurstSize);
			}
			return 0 + StepRoute(_public, now) + StepRoute(_admin, now);
		}

		private int StepRoute(Route r, double now)
		{
			if (!r.Configured)
			{
				return 0;
			}
			if (r.DisabledReason.Length > 0)
			{
				lock (_gate)
				{
					r.Queue.Clear();
				}
				return 0;
			}
			if (now < r.CircuitOpenUntil)
			{
				return 0;
			}
			if (r.CircuitOpenUntil > 0.0 && now >= r.CircuitOpenUntil)
			{
				r.CircuitOpenUntil = 0.0;
				r.ConsecutiveFailures = 0;
				r.LoggedPaused = false;
				_log("webhooks: the " + r.Name + " route is trying again");
			}
			if (now < r.NextAttemptAt)
			{
				return 0;
			}
			double num = Math.Max(0.01, _s.MessagesPerMinutePerRoute / 60.0);
			r.Tokens = Math.Min(_s.BurstSize, r.Tokens + (now - r.LastRefill) * num);
			r.LastRefill = now;
			if (r.Tokens < 1.0)
			{
				return 0;
			}
			if (r.Current == null)
			{
				WebhookEvent webhookEvent = null;
				int num2 = 0;
				lock (_gate)
				{
					if (r.Queue.Count == 0)
					{
						return 0;
					}
					webhookEvent = r.Queue.First.Value;
					r.Queue.RemoveFirst();
					if (Mergeable(webhookEvent.Type))
					{
						LinkedListNode<WebhookEvent> linkedListNode = r.Queue.First;
						while (linkedListNode != null)
						{
							LinkedListNode<WebhookEvent>? next = linkedListNode.Next;
							WebhookEvent value = linkedListNode.Value;
							if (value.Type == webhookEvent.Type && value.Code == webhookEvent.Code && (value.Utc - webhookEvent.Utc).TotalSeconds <= (double)_s.MergeWindowSeconds && webhookEvent.Count + num2 < 99)
							{
								num2 += value.Count;
								r.Queue.Remove(linkedListNode);
							}
							linkedListNode = next;
						}
					}
				}
				if (num2 > 0)
				{
					webhookEvent.Count += num2;
					Merged += num2;
				}
				WebhookMessage webhookMessage = MessageFormatter.Build(webhookEvent, r.Name == "admin", _s);
				r.Current = new Attempt
				{
					Message = webhookMessage,
					Json = MessageFormatter.ToJson(webhookMessage, _s)
				};
			}
			r.Tokens -= 1.0;
			PostResult postResult = Safe(() => _http.Post(r.Url(), r.Current.Json, Math.Max(1, _s.TimeoutSeconds)));
			r.Current.Attempts++;
			int result = 1;
			if (postResult.Ok)
			{
				Sent++;
				r.Current = null;
				r.ConsecutiveFailures = 0;
				return result;
			}
			if (postResult.Status == 429)
			{
				double num3 = Math.Max(1.0, Math.Min(300.0, (postResult.RetryAfterSeconds <= 0.0) ? 5.0 : postResult.RetryAfterSeconds));
				r.NextAttemptAt = now + num3;
				if (r.Current.Attempts >= _s.MaxAttempts * 3)
				{
					Failed++;
					r.Current = null;
				}
				return result;
			}
			if (postResult.Status == 401 || postResult.Status == 403 || postResult.Status == 404)
			{
				r.DisabledReason = "the webhook was refused (HTTP " + postResult.Status + ")";
				_log("webhooks: the " + r.Name + " route is switched off until the next restart: " + r.DisabledReason + ". Check the webhook URL in the configuration.");
				Failed++;
				r.Current = null;
				lock (_gate)
				{
					r.Queue.Clear();
					return result;
				}
			}
			if (postResult.Status == 400)
			{
				Failed++;
				r.Current = null;
				return result;
			}
			if (r.Current.Attempts >= _s.MaxAttempts)
			{
				Failed++;
				r.Current = null;
				r.ConsecutiveFailures++;
				if (r.ConsecutiveFailures >= _s.CircuitBreakerFailures)
				{
					r.CircuitOpenUntil = now + (double)_s.CircuitBreakerSeconds;
					if (!r.LoggedPaused)
					{
						r.LoggedPaused = true;
						_log("webhooks: the " + r.Name + " route failed " + r.ConsecutiveFailures + " times in a row and is paused for " + _s.CircuitBreakerSeconds + " s (last result: " + ((postResult.Status == 0) ? postResult.Error : ("HTTP " + postResult.Status)) + ")");
					}
				}
			}
			else
			{
				r.NextAttemptAt = now + Math.Min(60.0, 2.0 * Math.Pow(2.0, r.Current.Attempts - 1));
			}
			return result;
		}

		private static bool Mergeable(WebhookEventType t)
		{
			if (t != WebhookEventType.AdmissionRejected && t != WebhookEventType.CharacterRejected && t != WebhookEventType.CharacterDivergent)
			{
				return t == WebhookEventType.ServerError;
			}
			return true;
		}

		private PostResult Safe(Func<PostResult> f)
		{
			try
			{
				return f() ?? new PostResult
				{
					Status = 0,
					Error = "NullResult"
				};
			}
			catch (Exception ex)
			{
				return new PostResult
				{
					Status = 0,
					Error = ex.GetType().Name
				};
			}
		}

		public string Summary()
		{
			return "sent " + Sent + ", failed " + Failed + ", dropped " + Dropped + ", merged " + Merged + ", queued " + QueueDepth;
		}
	}
	public sealed class WebhookWorker : IDisposable
	{
		private readonly WebhookDispatcher _d;

		private readonly Func<double> _clock;

		private readonly Thread _thread;

		private readonly ManualResetEventSlim _wake = new ManualResetEventSlim(initialState: false);

		private volatile bool _stop;

		public bool Started { get; private set; }

		public WebhookWorker(WebhookDispatcher dispatcher, Func<double> clock = null)
		{
			_d = dispatcher;
			Stopwatch sw = Stopwatch.StartNew();
			_clock = clock ?? ((Func<double>)(() => sw.Elapsed.TotalSeconds));
			_thread = new Thread(Loop)
			{
				IsBackground = true,
				Name = "BSM webhooks",
				Priority = ThreadPriority.BelowNormal
			};
			if (_d.Enabled)
			{
				_thread.Start();
				Started = true;
			}
		}

		public void Wake()
		{
			_wake.Set();
		}

		private void Loop()
		{
			while (!_stop)
			{
				int num = 0;
				try
				{
					num = _d.Step(_clock());
				}
				catch
				{
				}
				_wake.Reset();
				_wake.Wait((num > 0) ? 50 : 500);
			}
		}

		public void Stop(double flushSeconds)
		{
			double num = _clock() + Math.Max(0.0, flushSeconds);
			while (_d.Enabled && (_d.QueueDepth > 0 || _d.Busy) && _clock() < num)
			{
				Thread.Sleep(20);
			}
			_stop = true;
			_wake.Set();
		}

		public void Dispose()
		{
			_stop = true;
			_wake.Set();
		}
	}
	public enum WebhookEventType
	{
		ServerStarted = 1,
		ServerStopping,
		PlayerJoined,
		PlayerLeft,
		AdmissionRejected,
		CharacterEnrolled,
		CharacterRejected,
		CharacterDivergent,
		CharacterReset,
		CharacterRemoved,
		AdminAction,
		ServerError,
		Test,
		AfkKick,
		AfkKickUnsaved
	}
	public sealed class WebhookEvent
	{
		public WebhookEventType Type;

		public DateTime Utc = DateTime.UtcNow;

		public string PlayerName = "";

		public string Steam64 = "";

		public string Code = "";

		public string Detail = "";

		public int Count = 1;

		public string Actor = "";
	}
	public sealed class WebhookSettings
	{
		public string PublicUrl = "";

		public string AdminUrl = "";

		public HashSet<WebhookEventType> PublicEvents = new HashSet<WebhookEventType>(new WebhookEventType[5]
		{
			WebhookEventType.ServerStarted,
			WebhookEventType.ServerStopping,
			WebhookEventType.PlayerJoined,
			WebhookEventType.PlayerLeft,
			WebhookEventType.CharacterEnrolled
		});

		public HashSet<WebhookEventType> AdminEvents = new HashSet<WebhookEventType>(new WebhookEventType[10]
		{
			WebhookEventType.AdmissionRejected,
			WebhookEventType.CharacterRejected,
			WebhookEventType.CharacterDivergent,
			WebhookEventType.CharacterReset,
			WebhookEventType.CharacterRemoved,
			WebhookEventType.AdminAction,
			WebhookEventType.ServerError,
			WebhookEventType.Test,
			WebhookEventType.AfkKick,
			WebhookEventType.AfkKickUnsaved
		});

		public bool IncludeSteamIdsOnAdminRoute = true;

		public string Username = "Beastheim";

		public string ServerName = "Beastheim";

		public int MaxQueue = 200;

		public double MessagesPerMinutePerRoute = 20.0;

		public int BurstSize = 5;

		public int TimeoutSeconds = 10;

		public int MaxAttempts = 3;

		public int CircuitBreakerFailures = 5;

		public int CircuitBreakerSeconds = 300;

		public int MergeWindowSeconds = 20;

		public List<string> AllowedHosts = new List<string> { "discord.com", "discordapp.com", "ptb.discord.com", "canary.discord.com" };

		public bool PublicConfigured => WebhookUrl.IsValid(PublicUrl, AllowedHosts);

		public bool AdminConfigured => WebhookUrl.IsValid(AdminUrl, AllowedHosts);

		public bool Enabled
		{
			get
			{
				if (!PublicConfigured)
				{
					return AdminConfigured;
				}
				return true;
			}
		}

		public static HashSet<WebhookEventType> ParseEvents(string list, out List<string> unknown)
		{
			unknown = new List<string>();
			HashSet<WebhookEventType> hashSet = new HashSet<WebhookEventType>();
			string[] array = (list ?? "").Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string text in array)
			{
				if (Enum.TryParse<WebhookEventType>(text.Trim(), ignoreCase: true, out var result) && Enum.IsDefined(typeof(WebhookEventType), result))
				{
					hashSet.Add(result);
				}
				else
				{
					unknown.Add(text.Trim());
				}
			}
			return hashSet;
		}

		public string Describe()
		{
			return "webhooks: public route " + (PublicConfigured ? "configured" : "not configured") + ", admin route " + (AdminConfigured ? "configured" : "not configured") + (Enabled ? "" : " (module idle)") + ((PublicUrl.Length > 0 && !PublicConfigured) ? " [public URL is not an allowed https webhook URL and is ignored]" : "") + ((AdminUrl.Length > 0 && !AdminConfigured) ? " [admin URL is not an allowed https webhook URL and is ignored]" : "");
		}
	}
	public static class WebhookUrl
	{
		public static bool IsValid(string url, IEnumerable<string> allowedHosts)
		{
			if (string.IsNullOrWhiteSpace(url))
			{
				return false;
			}
			if (!Uri.TryCreate(url.Trim(), UriKind.Absolute, out Uri u))
			{
				return false;
			}
			if (u.Scheme != Uri.UriSchemeHttps)
			{
				return false;
			}
			if (!allowedHosts.Any((string h) => string.Equals(h, u.Host, StringComparison.OrdinalIgnoreCase)))
			{
				return false;
			}
			if (u.AbsolutePath.StartsWith("/api/webhooks/", StringComparison.OrdinalIgnoreCase))
			{
				return u.AbsolutePath.Length > "/api/webhooks/".Length + 4;
			}
			return false;
		}
	}
	public static class Sanitizer
	{
		public static string Inline(string s, int maxChars)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(Math.Min(s.Length, maxChars + 4));
			bool flag = true;
			foreach (char c in s)
			{
				if (stringBuilder.Length >= maxChars)
				{
					if (stringBuilder.Length > 0 && char.IsHighSurrogate(stringBuilder[stringBuilder.Length - 1]))
					{
						stringBuilder.Length--;
					}
					stringBuilder.Append("…");
					break;
				}
				UnicodeCategory unicodeCategory = char.GetUnicodeCategory(c);
				if (unicodeCategory == UnicodeCategory.Control || unicodeCategory == UnicodeCategory.Format || unicodeCategory == UnicodeCategory.PrivateUse || unicodeCategory == UnicodeCategory.OtherNotAssigned || unicodeCategory == UnicodeCategory.LineSeparator || unicodeCategory == UnicodeCategory.ParagraphSeparator)
				{
					continue;
				}
				if (c == '`')
				{
					stringBuilder.Append('’');
					flag = false;
				}
				else if (char.IsWhiteSpace(c))
				{
					if (!flag)
					{
						stringBuilder.Append(' ');
					}
					flag = true;
				}
				else
				{
					stringBuilder.Append(c);
					flag = false;
				}
			}
			return stringBuilder.ToString().Trim();
		}

		public static string Code(string s, int maxChars)
		{
			string text = Inline(s, maxChars);
			if (text.Length != 0)
			{
				return "`" + text + "`";
			}
			return "`?`";
		}

		public static string CodeToken(string s)
		{
			if (string.IsNullOrEmpty(s))
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder();
			foreach (char c in s)
			{
				if (stringBuilder.Length >= 48)
				{
					break;
				}
				if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '_' || c == ':' || c == '-')
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}
	}
	public static class Json
	{
		public static string Escape(string s)
		{
			if (s == null)
			{
				return "";
			}
			StringBuilder stringBuilder = new StringBuilder(s.Length + 8);
			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 < ' ' || c == '\u2028' || c == '\u2029')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}
	}
	public sealed class WebhookMessage
	{
		public string Title = "";

		public string Description = "";

		public int Color;

		public DateTime Utc;

		public string Footer = "";
	}
	public static class MessageFormatter
	{
		public const int ColorGreen = 4437377;

		public const int ColorRed = 15746887;

		public const int ColorAmber = 16426522;

		public const int ColorBlue = 7506394;

		public const int ColorGrey = 7634829;

		public const int ColorPurple = 9133302;

		public static WebhookMessage Build(WebhookEvent e, bool adminRoute, WebhookSettings s)
		{
			WebhookMessage webhookMessage = new WebhookMessage
			{
				Utc = e.Utc,
				Footer = Sanitizer.Inline(s.ServerName, 40)
			};
			string text = Sanitizer.Code(e.PlayerName, 24);
			string text2 = ((e.Count > 1) ? (" (x" + e.Count + ")") : "");
			string text3 = ((adminRoute && s.IncludeSteamIdsOnAdminRoute && e.Steam64.Length > 0) ? ("\nSteam ID: `" + Sanitizer.CodeToken(e.Steam64) + "`") : "");
			string text4 = ((e.Code.Length > 0) ? ("\nCode: `" + Sanitizer.CodeToken(e.Code) + "`") : "");
			string text5 = ((e.Detail.Length > 0) ? ("\n" + Sanitizer.Inline(e.Detail, 180)) : "");
			switch (e.Type)
			{
			case WebhookEventType.ServerStarted:
				webhookMessage.Title = "Server online";
				webhookMessage.Description = "The server is up and accepting players.";
				webhookMessage.Color = 4437377;
				break;
			case WebhookEventType.ServerStopping:
				webhookMessage.Title = "Server stopping";
				webhookMessage.Description = "The server is shutting down.";
				webhookMessage.Color = 15746887;
				break;
			case WebhookEventType.PlayerJoined:
				webhookMessage.Title = "Player joined";
				webhookMessage.Description = text + " entered the world." + text3;
				webhookMessage.Color = 4437377;
				break;
			case WebhookEventType.PlayerLeft:
				webhookMessage.Title = "Player left";
				webhookMessage.Description = text + " left the world." + text3;
				webhookMessage.Color = 16426522;
				break;
			case WebhookEventType.CharacterEnrolled:
				webhookMessage.Title = "A new Viking arrived";
				webhookMessage.Description = text + " was registered." + text3;
				webhookMessage.Color = 7506394;
				break;
			case WebhookEventType.AdmissionRejected:
				webhookMessage.Title = "Mod check failed" + text2;
				webhookMessage.Description = ((e.PlayerName.Length > 0) ? (text + " was refused.") : "A player was refused.") + text4 + text5 + text3;
				webhookMessage.Color = 15746887;
				break;
			case WebhookEventType.CharacterRejected:
				webhookMessage.Title = "Character refused" + text2;
				webhookMessage.Description = ((e.PlayerName.Length > 0) ? (text + " was refused.") : "A character was refused.") + text4 + text3;
				webhookMessage.Color = 15746887;
				break;
			case WebhookEventType.CharacterDivergent:
				webhookMessage.Title = "Character does not match the server's record" + text2;
				webhookMessage.Description = ((e.PlayerName.Length > 0) ? text : "A character") + " was refused; the file was kept for review." + text4 + text3;
				webhookMessage.Color = 16426522;
				break;
			case WebhookEventType.CharacterReset:
				webhookMessage.Title = "Character reset";
				webhookMessage.Description = text + " was archived." + text3;
				webhookMessage.Color = 9133302;
				break;
			case WebhookEventType.CharacterRemoved:
				webhookMessage.Title = "Player removed after admission";
				webhookMessage.Description = ((e.PlayerName.Length > 0) ? text : "A player") + " was removed." + text4 + text3;
				webhookMessage.Color = 15746887;
				break;
			case WebhookEventType.AdminAction:
				webhookMessage.Title = "Administrator action";
				webhookMessage.Description = ((e.Actor.Length > 0) ? ("By `" + Sanitizer.Inline(e.Actor, 40) + "`: ") : "") + Sanitizer.Inline(e.Detail, 200) + text4 + text3;
				webhookMessage.Color = 9133302;
				break;
			case WebhookEventType.AfkKick:
				webhookMessage.Title = "Idle player removed";
				webhookMessage.Description = text + " was idle and was disconnected after the character was saved." + text5 + text3;
				webhookMessage.Color = 16426522;
				break;
			case WebhookEventType.AfkKickUnsaved:
				webhookMessage.Title = "Idle player removed without a confirmed save";
				webhookMessage.Description = text + " was idle and was disconnected, but the server did not receive a final save. The last acknowledged character revision stands." + text5 + text3;
				webhookMessage.Color = 15746887;
				break;
			case WebhookEventType.ServerError:
				webhookMessage.Title = "Server problem";
				webhookMessage.Description = Sanitizer.Inline(e.Detail, 220) + text4;
				webhookMessage.Color = 15746887;
				break;
			default:
				webhookMessage.Title = "Webhook test";
				webhookMessage.Description = "This is a test message from Beastheim Server Mods.";
				webhookMessage.Color = 7634829;
				break;
			}
			return webhookMessage;
		}

		public static string ToJson(WebhookMessage m, WebhookSettings s)
		{
			StringBuilder stringBuilder = new StringBuilder(512);
			stringBuilder.Append("{\"username\":\"").Append(Json.Escape(Sanitizer.Inline(s.Username, 32))).Append("\",\"allowed_mentions\":{\"parse\":[]},\"embeds\":[{");
			stringBuilder.Append("\"title\":\"").Append(Json.Escape(Trim(m.Title, 256))).Append("\",");
			stringBuilder.Append("\"description\":\"").Append(Json.Escape(Trim(m.Description, 1900))).Append("\",");
			stringBuilder.Append("\"color\":").Append(m.Color).Append(',');
			stringBuilder.Append("\"timestamp\":\"").Append(m.Utc.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)).Append("\",");
			stringBuilder.Append("\"footer\":{\"text\":\"").Append(Json.Escape(Trim(m.Footer, 200))).Append("\"}}]}");
			return stringBuilder.ToString();
		}

		private static string Trim(string s, int max)
		{
			s = s ?? "";
			if (s.Length <= max)
			{
				return s;
			}
			int num = max - 1;
			if (num > 0 && char.IsHighSurrogate(s[num - 1]))
			{
				num--;
			}
			return s.Substring(0, num) + "…";
		}
	}
}
namespace Beastheim.ServerMods.Characters.Transfer
{
	public sealed class TransferTooLargeException : Exception
	{
		public TransferTooLargeException(int length)
			: base("character file of " + length + " bytes exceeds the supported maximum of " + 1048576 + " bytes and will not be transmitted")
		{
		}
	}
	public sealed class ChunkSender
	{
		private readonly byte[] _file;

		private readonly double _intervalSeconds;

		private int _next;

		private double _nextAt = double.NegativeInfinity;

		public TransferKind Kind { get; private set; }

		public long TransferId { get; private set; }

		public long CharacterId { get; private set; }

		public int ParentSeq { get; private set; }

		public byte[] Sha256 { get; private set; }

		public int TotalLength => _file.Length;

		public int ChunkCount { get; private set; }

		public int ChunksSent => _next;

		public bool AllSent => _next >= ChunkCount;

		public ChunkSender(TransferKind kind, long characterId, int parentSeq, byte[] file, long transferId, int intervalMs = 110)
		{
			if (file == null || file.Length < 1)
			{
				throw new ArgumentException("empty character file");
			}
			if (file.Length > 1048576)
			{
				throw new TransferTooLargeException(file.Length);
			}
			if (transferId == 0L)
			{
				throw new ArgumentException("transfer id must be non-zero");
			}
			if (parentSeq < 0)
			{
				throw new ArgumentException("negative parent revision");
			}
			_file = file;
			_intervalSeconds = (double)Math.Max(20, intervalMs) / 1000.0;
			Kind = kind;
			TransferId = transferId;
			CharacterId = characterId;
			ParentSeq = parentSeq;
			using (SHA256 sHA = SHA256.Create())
			{
				Sha256 = sHA.ComputeHash(file);
			}
			ChunkCount = (file.Length + 16384 - 1) / 16384;
		}

		public static long NewTransferId()
		{
			byte[] array = new byte[8];
			using RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create();
			long num;
			do
			{
				randomNumberGenerator.GetBytes(array);
				num = BitConverter.ToInt64(array, 0) & 0x7FFFFFFFFFFFFFFFL;
			}
			while (num == 0L);
			return num;
		}

		public bool TryNext(double now, out ChunkMessage chunk)
		{
			chunk = null;
			if (AllSent || now < _nextAt)
			{
				return false;
			}
			int num = _next * 16384;
			int num2 = Math.Min(16384, _file.Length - num);
			byte[] array = new byte[num2];
			Buffer.BlockCopy(_file, num, array, 0, num2);
			chunk = new ChunkMessage
			{
				Kind = Kind,
				TransferId = TransferId,
				CharacterId = CharacterId,
				ParentSeq = ParentSeq,
				TotalLength = _file.Length,
				Sha256 = Sha256,
				Index = _next,
				Count = ChunkCount,
				Payload = array
			};
			_next++;
			_nextAt = now + _intervalSeconds;
			return true;
		}
	}
	public sealed class ChunkReceiver
	{
		public enum Status
		{
			InProgress,
			Complete,
			Error
		}

		public sealed class Result
		{
			public Status Status;

			public string Error = "";

			public byte[] File;

			public long TransferId;

			public TransferKind Kind;

			public long CharacterId;

			public int ParentSeq;

			public byte[] Sha256;

			public long AbandonedTransferId;
		}

		public const double TransferTimeoutSeconds = 30.0;

		private byte[] _buffer;

		private int _filled;

		private int _expectedIndex;

		private ChunkMessage _first;

		private double _lastChunkAt;

		public bool Active => _first != null;

		public long ActiveTransferId
		{
			get
			{
				if (_first != null)
				{
					return _first.TransferId;
				}
				return 0L;
			}
		}

		public int ChunksReceived => _expectedIndex;

		public Result Feed(ChunkMessage c, double now)
		{
			long abandonedTransferId = 0L;
			if (_first != null && c.TransferId != _first.TransferId)
			{
				if (c.Index != 0)
				{
					return Fail(c, "chunk " + c.Index + " of transfer " + c.TransferId + " arrived while transfer " + _first.TransferId + " is still open", resetState: false);
				}
				abandonedTransferId = _first.TransferId;
				Reset();
			}
			if (_first == null)
			{
				if (c.Index != 0)
				{
					return Fail(c, "transfer " + c.TransferId + " started at chunk " + c.Index + " instead of 0", resetState: false);
				}
				_first = c;
				_buffer = new byte[c.TotalLength];
				_filled = 0;
				_expectedIndex = 0;
			}
			else if (c.Kind != _first.Kind || c.CharacterId != _first.CharacterId || c.ParentSeq != _first.ParentSeq || c.TotalLength != _first.TotalLength || c.Count != _first.Count || !SameHash(c.Sha256, _first.Sha256))
			{
				return Fail(c, "chunk header differs from the transfer's first chunk", resetState: true);
			}
			if (c.Index != _expectedIndex)
			{
				return Fail(c, "chunk " + c.Index + " arrived but chunk " + _expectedIndex + " was expected", resetState: true);
			}
			Buffer.BlockCopy(c.Payload, 0, _buffer, _filled, c.Payload.Length);
			_filled += c.Payload.Length;
			_expectedIndex++;
			_lastChunkAt = now;
			Result result = new Result
			{
				TransferId = c.TransferId,
				Kind = c.Kind,
				CharacterId = c.CharacterId,
				ParentSeq = c.ParentSeq,
				Sha256 = c.Sha256,
				AbandonedTransferId = abandonedTransferId
			};
			if (_expectedIndex < c.Count)
			{
				result.Status = Status.InProgress;
				return result;
			}
			if (_filled != c.TotalLength)
			{
				return Fail(c, "assembled " + _filled + " bytes, declared " + c.TotalLength, resetState: true);
			}
			byte[] a;
			using (SHA256 sHA = SHA256.Create())
			{
				a = sHA.ComputeHash(_buffer);
			}
			if (!SameHash(a, c.Sha256))
			{
				return Fail(c, "assembled file does not match the declared SHA-256", resetState: true);
			}
			byte[] buffer = _buffer;
			Reset();
			result.Status = Status.Complete;
			result.File = buffer;
			return result;
		}

		public long Sweep(double now)
		{
			if (_first == null || now - _lastChunkAt < 30.0)
			{
				return 0L;
			}
			long transferId = _first.TransferId;
			Reset();
			return transferId;
		}

		public long Abort()
		{
			long result = ((_first == null) ? 0 : _first.TransferId);
			Reset();
			return result;
		}

		private Result Fail(ChunkMessage c, string error, bool resetState)
		{
			if (resetState)
			{
				Reset();
			}
			return new Result
			{
				Status = Status.Error,
				Error = error,
				TransferId = c.TransferId,
				Kind = c.Kind,
				CharacterId = c.CharacterId
			};
		}

		private void Reset()
		{
			_first = null;
			_buffer = null;
			_filled = 0;
			_expectedIndex = 0;
		}

		private static bool SameHash(byte[] a, byte[] b)
		{
			if (a == null || b == null || a.Length != b.Length)
			{
				return false;
			}
			int num = 0;
			for (int i = 0; i < a.Length; i++)
			{
				num |= a[i] ^ b[i];
			}
			return num == 0;
		}
	}
}
namespace Beastheim.ServerMods.Characters.Trace
{
	public interface ICharacterTrace
	{
		void Event(string name, params object[] keyValues);
	}
	public sealed class NullTrace : ICharacterTrace
	{
		public static readonly NullTrace Instance = new NullTrace();

		public void Event(string name, params object[] keyValues)
		{
		}
	}
	public sealed class ListTrace : ICharacterTrace
	{
		public sealed class Item
		{
			public string Name;

			public Dictionary<string, string> Values = new Dictionary<string, string>(StringComparer.Ordinal);

			public int Seq;

			public double TimeSeconds;

			public string Get(string key)
			{
				if (!Values.TryGetValue(key, out var value))
				{
					return null;
				}
				return value;
			}
		}

		public readonly List<Item> Items = new List<Item>();

		public Func<double> Clock = () => 0.0;

		public void Event(string name, params object[] kv)
		{
			Item item = new Item
			{
				Name = name,
				Seq = Items.Count + 1,
				TimeSeconds = Clock()
			};
			TraceFormat.Fill(item.Values, kv);
			Items.Add(item);
		}

		public List<Item> Named(string name)
		{
			return Items.FindAll((Item i) => i.Name == name);
		}

		public int IndexOf(Predicate<Item> p)
		{
			return Items.FindIndex(p);
		}
	}
	public sealed class StampedTrace : ICharacterTrace
	{
		private readonly string _role;

		private readonly Action<string> _sink;

		private readonly Func<DateTime> _utc;

		private readonly Func<double> _monoMs;

		private int _seq;

		public StampedTrace(string role, Action<string> sink, Func<DateTime> utc = null, Func<double> monoMs = null)
		{
			_role = role;
			_sink = sink;
			_utc = utc ?? ((Func<DateTime>)(() => DateTime.UtcNow));
			Stopwatch sw = Stopwatch.StartNew();
			_monoMs = monoMs ?? ((Func<double>)(() => sw.Elapsed.TotalMilliseconds));
		}

		public void Event(string name, params object[] keyValues)
		{
			string[] obj = new string[9]
			{
				_utc().ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture),
				"Z #",
				null,
				null,
				null,
				null,
				null,
				null,
				null
			};
			int num = ++_seq;
			obj[2] = num.ToString(CultureInfo.InvariantCulture);
			obj[3] = " mono=";
			obj[4] = Math.Round(_monoMs()).ToString("0", CultureInfo.InvariantCulture);
			obj[5] = " ";
			obj[6] = _role;
			obj[7] = " ";
			obj[8] = TraceFormat.Line(name, keyValues);
			string obj2 = string.Concat(obj);
			try
			{
				_sink(obj2);
			}
			catch (Exception)
			{
			}
		}
	}
	public static class TraceFormat
	{
		public static string Short(byte[] sha256)
		{
			if (sha256 == null || sha256.Length < 8)
			{
				return "none";
			}
			StringBuilder stringBuilder = new StringBuilder(16);
			for (int i = 0; i < 8; i++)
			{
				stringBuilder.Append(sha256[i].ToString("x2", CultureInfo.InvariantCulture));
			}
			return stringBuilder.ToString();
		}

		public static string Value(object o)
		{
			if (o == null)
			{
				return "null";
			}
			if (o is bool)
			{
				if (!(bool)o)
				{
					return "false";
				}
				return "true";
			}
			if (o is double num)
			{
				return num.ToString("0.###", CultureInfo.InvariantCulture);
			}
			if (o is float num2)
			{
				return num2.ToString("0.###", CultureInfo.InvariantCulture);
			}
			string text = Convert.ToString(o, CultureInfo.InvariantCulture) ?? "";
			if (text.Length == 0)
			{
				return "\"\"";
			}
			bool flag = false;
			string text2 = text;
			foreach (char c in text2)
			{
				if (c == ' ' || c == '=' || c == '"' || c < ' ')
				{
					flag = true;
					break;
				}
			}
			if (!flag)
			{
				return text;
			}
			return "\"" + text.Replace("\\", "\\\\").Replace("\"", "'").Replace("\r", " ")
				.Replace("\n", " ") + "\"";
		}

		public static void Fill(Dictionary<string, string> into, object[] kv)
		{
			for (int i = 0; i + 1 < kv.Length; i += 2)
			{
				into[Convert.ToString(kv[i], CultureInfo.InvariantCulture)] = Value(kv[i + 1]);
			}
		}

		public static string Line(string name, object[] kv)
		{
			StringBuilder stringBuilder = new StringBuilder("EVT ").Append(name);
			for (int i = 0; i + 1 < kv.Length; i += 2)
			{
				stringBuilder.Append(' ').Append(Convert.ToString(kv[i], CultureInfo.InvariantCulture)).Append('=')
					.Append(Value(kv[i + 1]));
			}
			return stringBuilder.ToString();
		}
	}
}
namespace Beastheim.ServerMods.Characters.Store
{
	public interface IStoreFs
	{
		void CreateDirectory(string path);

		void WriteDurable(string path, byte[] data);

		void Move(string from, string to);

		byte[] ReadAllBytes(string path);

		bool Exists(string path);

		void Delete(string path);

		string[] GetFiles(string dir, string pattern);

		string[] GetDirectories(string dir);

		void AppendDurable(string path, byte[] data);

		void TruncateTo(string path, long length);

		void MoveDirectory(string from, string to);

		void DeleteDirectory(string path);

		bool DirectoryExists(string path);

		long FileLength(string path);
	}
	public sealed class RealStoreFs : IStoreFs
	{
		public void CreateDirectory(string path)
		{
			Directory.CreateDirectory(path);
		}

		public void WriteDurable(string path, byte[] data)
		{
			using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None);
			fileStream.Write(data, 0, data.Length);
			fileStream.Flush(flushToDisk: true);
		}

		public void Move(string from, string to)
		{
			File.Move(from, to);
		}

		public byte[] ReadAllBytes(string path)
		{
			return File.ReadAllBytes(path);
		}

		public bool Exists(string path)
		{
			return File.Exists(path);
		}

		public void Delete(string path)
		{
			if (File.Exists(path))
			{
				File.Delete(path);
			}
		}

		public string[] GetFiles(string dir, string pattern)
		{
			if (!Directory.Exists(dir))
			{
				return new string[0];
			}
			return Directory.GetFiles(dir, pattern);
		}

		public string[] GetDirectories(string dir)
		{
			if (!Directory.Exists(dir))
			{
				return new string[0];
			}
			return Directory.GetDirectories(dir);
		}

		public void AppendDurable(string path, byte[] data)
		{
			using FileStream fileStream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
			fileStream.Write(data, 0, data.Length);
			fileStream.Flush(flushToDisk: true);
		}

		public void TruncateTo(string path, long length)
		{
			using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.Read);
			fileStream.SetLength(length);
			fileStream.Flush(flushToDisk: true);
		}

		public void MoveDirectory(string from, string to)
		{
			Directory.CreateDirectory(Path.GetDirectoryName(to));
			Directory.Move(from, to);
		}

		public void DeleteDirectory(string path)
		{
			if (Directory.Exists(path))
			{
				Directory.Delete(path, recursive: true);
			}
		}

		public bool DirectoryExists(string path)
		{
			return Directory.Exists(path);
		}

		public long FileLength(string path)
		{
			return new FileInfo(path).Length;
		}
	}
	public sealed class RegisteredCharacter
	{
		public string Steam64;

		public long CharacterId;

		public string Name = "";

		public string NameKey = "";

		public int HeadSeq;

		public byte[] HeadSha256 = new byte[0];

		public int HeadLength;

		public DateTime HeadCommittedUtc;

		public DateTime FirstCommittedUtc;
	}
	public interface ICharacterIndex
	{
		IList<RegisteredCharacter> GetAccountCharacters(string steam64);

		string FindOwnerOfCharacterId(long characterId);

		RegisteredCharacter FindByNameKey(string nameKey);
	}
	public enum CommitStatus
	{
		Committed,
		Unchanged,
		Invalid,
		Failed
	}
	public sealed class CommitResult
	{
		public CommitStatus Status;

		public int Seq;

		public byte[] Sha256 = new byte[0];

		public string Error = "";

		public ParsedCharacter Parsed;
	}
	public sealed class RevisionStore : IDisposable, ICharacterIndex
	{
		private sealed class HeadInfo
		{
			public int Seq;

			public byte[] Sha;

			public int Length;

			public string Name;

			public DateTime Committed;
		}

		public const int KeepNewest = 50;

		public const int KeepDailyDays = 30;

		private readonly string _root;

		private readonly IStoreFs _fs;

		private readonly Func<DateTime> _utcNow;

		private readonly Action<string> _warn;

		private readonly object _gate = new object();

		private FileStream _lock;

		private readonly Dictionary<string, Dictionary<long, RegisteredCharacter>> _byAccount = new Dictionary<string, Dictionary<long, RegisteredCharacter>>(StringComparer.Ordinal);

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

		private readonly Dictionary<string, RegisteredCharacter> _byName = new Dictionary<string, RegisteredCharacter>(StringComparer.Ordinal);

		public RevisionStore(string root, Func<DateTime> utcNow = null, IStoreFs fs = null, Action<string> warn = null, bool takeWriterLock = true)
		{
			_root = root;
			_utcNow = utcNow ?? ((Func<DateTime>)(() => DateTime.UtcNow));
			_fs = fs ?? new RealStoreFs();
			_warn = warn ?? ((Action<string>)delegate
			{
			});
			_fs.CreateDirectory(root);
			if (takeWriterLock)
			{
				try
				{
					_lock = new FileStream(Path.Combine(root, ".writer-lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
				}
				catch (IOException ex)
				{
					throw new IOException("Another process holds the character store lock: " + ex.Message, ex);
				}
			}
			Rebuild();
		}

		public void Dispose()
		{
			if (_lock != null)
			{
				_lock.Dispose();
				_lock = null;
			}
		}

		public static bool IsValidSteam64(string s)
		{
			if (s == null || s.Length != 17)
			{
				return false;
			}
			foreach (char c in s)
			{
				if (c < '0' || c > '9')
				{
					return false;
				}
			}
			return s.StartsWith("7656119", StringComparison.Ordinal);
		}

		public static string Hex(byte[] b)
		{
			StringBuilder stringBuilder = new StringBuilder(b.Length * 2);
			foreach (byte b2 in b)
			{
				stringBuilder.Append(b2.ToString("x2", CultureInfo.InvariantCulture));
			}
			return stringBuilder.ToString();
		}

		private static byte[] Sha(byte[] data)
		{
			using SHA256 sHA = SHA256.Create();
			return sHA.ComputeHash(data);
		}

		private string CharDir(string steam64, long id)
		{
			return Path.Combine(Path.Combine(_root, steam64), id.ToString(CultureInfo.InvariantCulture));
		}

		private static string Stem(string dir, int seq)
		{
			return Path.Combine(dir, "rev-" + seq.ToString("000000", CultureInfo.InvariantCulture));
		}

		public IList<RegisteredCharacter> GetAccountCharacters(string steam64)
		{
			lock (_gate)
			{
				if (steam64 == null || !_byAccount.TryGetValue(steam64, out var value))
				{
					return new List<RegisteredCharacter>();
				}
				return (from c in value.Values
					orderby c.FirstCommittedUtc, c.CharacterId
					select c).Select(Clone).ToList();
			}
		}

		public string FindOwnerOfCharacterId(long characterId)
		{
			lock (_gate)
			{
				string value;
				return _owner.TryGetValue(characterId, out value) ? value : null;
			}
		}

		public RegisteredCharacter FindByNameKey(string nameKey)
		{
			lock (_gate)
			{
				RegisteredCharacter value;
				return (nameKey != null && _byName.TryGetValue(nameKey, out value)) ? Clone(value) : null;
			}
		}

		private static RegisteredCharacter Clone(RegisteredCharacter c)
		{
			return new RegisteredCharacter
			{
				Steam64 = c.Steam64,
				CharacterId = c.CharacterId,
				Name = c.Name,
				NameKey = c.NameKey,
				HeadSeq = c.HeadSeq,
				HeadSha256 = (byte[])c.HeadSha256.Clone(),
				HeadLength = c.HeadLength,
				HeadCommittedUtc = c.HeadCommittedUtc,
				FirstCommittedUtc = c.FirstCommittedUtc
			};
		}

		public byte[] ReadRevision(string steam64, long characterId, int seq)
		{
			lock (_gate)
			{
				string text = Stem(CharDir(steam64, characterId), seq);
				if (!_fs.Exists(text + ".ok") || !_fs.Exists(text + ".fch"))
				{
					return null;
				}
				byte[] array = _fs.ReadAllBytes(text + ".fch");
				return (Hex(Sha(array)) == Encoding.ASCII.GetString(_fs.ReadAllBytes(text + ".ok")).Trim()) ? array : null;
			}
		}

		public List<int> ListCommittedRevisions(string steam64, long characterId)
		{
			lock (_gate)
			{
				return CommittedSeqs(CharDir(steam64, characterId));
			}
		}

		public CommitResult Commit(string steam64, long characterId, byte[] file, int parentSeq, TransferKind kind)
		{
			lock (_gate)
			{
				try
				{
					return CommitCore(steam64, characterId, file, parentSeq, kind);
				}
				catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
				{
					return new CommitResult
					{
						Status = CommitStatus.Failed,
						Error = "storage error: " + ex.GetType().Name + ": " + ex.Message
					};
				}
			}
		}

		private CommitResult CommitCore(string steam64, long characterId, byte[] file, int parentSeq, TransferKind kind)
		{
			if (!IsValidSteam64(steam64))
			{
				return Invalid("not a Steam64 ID");
			}
			if (file == null || file.Length < 1 || file.Length > 1048576)
			{
				return Invalid("file size outside 1.." + 1048576 + " bytes");
			}
			FchParseResult fchParseResult = FchParser.Parse(file);
			if (!fchParseResult.Ok)
			{
				return new CommitResult
				{
					Status = CommitStatus.Invalid,
					Error = "not a valid character file: " + fchParseResult.Error
				};
			}
			ParsedCharacter character = fchParseResult.Character;
			if (character.PlayerId != characterId)
			{
				return new CommitResult
				{
					Status = CommitStatus.Invalid,
					Error = "the file's CharacterID " + character.PlayerId + " does not match the transfer's " + characterId,
					Parsed = character
				};
			}
			if (string.IsNullOrEmpty(character.Name))
			{
				return new CommitResult
				{
					Status = CommitStatus.Invalid,
					Error = "the file has no character name",
					Parsed = character
				};
			}
			string text = CharDir(steam64, characterId);
			_fs.CreateDirectory(text);
			CleanupUncommitted(text);
			byte[] array = Sha(file);
			List<int> list = CommittedSeqs(text);
			HeadInfo headInfo = LoadHead(text, list);
			if (headInfo != null && headInfo.Sha.SequenceEqual(array))
			{
				return new CommitResult
				{
					Status = CommitStatus.Unchanged,
					Seq = headInfo.Seq,
					Sha256 = array,
					Parsed = character
				};
			}
			int seq = ((list.Count != 0) ? list.Max() : 0) + 1;
			string text2 = Stem(text, seq);
			DateTime committed = _utcNow();
			_fs.WriteDurable(text2 + ".fch.tmp", file);
			if (!Sha(_fs.ReadAllBytes(text2 + ".fch.tmp")).SequenceEqual(array))
			{
				_fs.Delete(text2 + ".fch.tmp");
				return new CommitResult
				{
					Status = CommitStatus.Failed,
					Error = "read-back verification failed"
				};
			}
			_fs.Move(text2 + ".fch.tmp", text2 + ".fch");
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("steam64=").Append(steam64).Append('\n');
			stringBuilder.Append("characterId=").Append(characterId.ToString(CultureInfo.InvariantCulture)).Append('\n');
			stringBuilder.Append("name=").Append(character.Name.Replace('\n', ' ').Replace('\r', ' ')).Append('\n');
			stringBuilder.Append("seq=").Append(seq.ToString(CultureInfo.InvariantCulture)).Append('\n');
			stringBuilder.Append("parentSeq=").Append(parentSeq.ToString(CultureInfo.InvariantCulture)).Append('\n');
			stringBuilder.Append("kind=").Append(kind.ToString()).Append('\n');
			stringBuilder.Append("sha256=").Append(Hex(array)).Append('\n');
			stringBuilder.Append("length=").Append(file.Length.ToString(CultureInfo.InvariantCulture)).Append('\n');
			stringBuilder.Append("committedUtc=").Append(committed.ToString("o", CultureInfo.InvariantCulture)).Append('\n');
			stringBuilder.Append("profileVersion=").Append(character.ProfileVersion.ToString(CultureInfo.InvariantCulture)).Append('\n');
			stringBuilder.Append("firstSpawn=").Append(character.FirstSpawn ? "1" : "0").Append('\n');
			_fs.WriteDurable(text2 + ".meta.tmp", Encoding.UTF8.GetBytes(stringBuilder.ToString()));
			_fs.Move(text2 + ".meta.tmp", text2 + ".meta");
			_fs.WriteDurable(text2 + ".ok.tmp", Encoding.ASCII.GetBytes(Hex(array)));
			_fs.Move(text2 + ".ok.tmp", text2 + ".ok");
			UpdateIndex(steam64, characterId, character.Name, seq, array, file.Length, committed);
			return new CommitResult
			{
				Status = CommitStatus.Committed,
				Seq = seq,
				Sha256 = array,
				Parsed = character
			};
		}

		private static CommitResult Invalid(string why)
		{
			return new CommitResult
			{
				Status = CommitStatus.Invalid,
				Error = why
			};
		}

		private List<int> CommittedSeqs(string dir)
		{
			List<int> list = new List<int>();
			string[] files = _fs.GetFiles(dir, "rev-*.ok");
			for (int i = 0; i < files.Length; i++)
			{
				string fileName = Path.GetFileName(files[i]);
				if (fileName.Length == "rev-000000.ok".Length && int.TryParse(fileName.Substring(4, 6), NumberStyles.None, CultureInfo.InvariantCulture, out var result) && result > 0)
				{
					list.Add(result);
				}
			}
			list.Sort();
			return list;
		}

		private HeadInfo LoadHead(string dir, List<int> seqs)
		{
			for (int num = seqs.Count - 1; num >= 0; num--)
			{
				string text = Stem(dir, seqs[num]);
				try
				{
					if (!_fs.Exists(text + ".fch"))
					{
						_warn("revision " + seqs[num] + " in " + dir + " has a commit marker but no data file; skipped");
					}
					else
					{
						byte[] array = _fs.ReadAllBytes(text + ".fch");
						byte[] array2 = Sha(array);
						if (!(Hex(array2) != Encoding.ASCII.GetString(_fs.ReadAllBytes(text + ".ok")).Trim()))
						{
							Dictionary<string, string> dictionary = ReadMeta(text + ".meta");
							if (!dictionary.ContainsKey("committedUtc") || !DateTime.TryParse(dictionary["committedUtc"], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result))
							{
								result = DateTime.MinValue;
							}
							return new HeadInfo
							{
								Seq = seqs[num],
								Sha = array2,
								Length = array.Length,
								Name = (dictionary.ContainsKey("name") ? dictionary["name"] : ""),
								Committed = result
							};
						}
						_warn("revision " + seqs[num] + " in " + dir + " fails its hash; skipped");
					}
				}
				catch (IOException ex)
				{
					_warn("revision " + seqs[num] + " in " + dir + " unreadable: " + ex.Message);
				}
			}
			return null;
		}

		private Dictionary<string, string> ReadMeta(string path)
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
			if (!_fs.Exists(path))
			{
				return dictionary;
			}
			string[] array = Encoding.UTF8.GetString(_fs.ReadAllBytes(path)).Split(new char[1] { '\n' });
			foreach (string text in array)
			{
				int num = text.IndexOf('=');
				if (num > 0)
				{
					dictionary[text.Substring(0, num)] = text.Substring(num + 1);
				}
			}
			return dictionary;
		}

		private void CleanupUncommitted(string dir)
		{
			HashSet<int> hashSet = new HashSet<int>(CommittedSeqs(dir));
			string[] files = _fs.GetFiles(dir, "*");
			foreach (string path in files)
			{
				string fileName = Path.GetFileName(path);
				int result;
				if (fileName.EndsWith(".tmp", StringComparison.Ordinal))
				{
					SafeDelete(path);
				}
				else if (fileName.StartsWith("rev-", StringComparison.Ordinal) && fileName.Length > 10 && int.TryParse(fileName.Substring(4, 6), NumberStyles.None, CultureInfo.InvariantCulture, out result) && !hashSet.Contains(result))
				{
					SafeDelete(path);
				}
			}
		}

		private void SafeDelete(string path)
		{
			try
			{
				_fs.Delete(path);
			}
			catch (IOException)
			{
			}
			catch (UnauthorizedAccessException)
			{
			}
		}

		public void Rebuild()
		{
			lock (_gate)
			{
				_byAccount.Clear();
				_owner.Clear();
				_byName.Clear();
				string[] directories = _fs.GetDirectories(_root);
				foreach (string text in directories)
				{
					string fileName = Path.GetFileName(text);
					if (!IsValidSteam64(fileName))
					{
						continue;
					}
					string[] directories2 = _fs.GetDirectories(text);
					foreach (string text2 in directories2)
					{
						if (!long.TryParse(Path.GetFileName(text2), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result))
						{
							continue;
						}
						CleanupUncommitted(text2);
						List<int> list = CommittedSeqs(text2);
						HeadInfo headInfo = LoadHead(text2, list);
						if (headInfo == null)
						{
							continue;
						}
						DateTime value = DateTime.MinValue;
						if (list.Count > 0)
						{
							Dictionary<string, string> dictionary = ReadMeta(Stem(text2, list[0]) + ".meta");
							if (dictionary.ContainsKey("committedUtc") && DateTime.TryParse(dictionary["committedUtc"], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result2))
							{
								value = result2;
							}
						}
						UpdateIndex(fileName, result, headInfo.Name, headInfo.Seq, headInfo.Sha, headInfo.Length, headInfo.Committed, value);
					}
				}
			}
		}

		private void UpdateIndex(string steam, long id, string name, int seq, byte[] sha, int length, DateTime committed, DateTime? first = null)
		{
			if (!_byAccount.TryGetValue(steam, out var value))
			{
				value = new Dictionary<long, RegisteredCharacter>();
				_byAccount[steam] = value;
			}
			if (!value.TryGetValue(id, out var value2))
			{
				RegisteredCharacter obj = new RegisteredCharacter
				{
					Steam64 = steam,
					CharacterId = id,
					FirstCommittedUtc = (first ?? committed)
				};
				value2 = (value[id] = obj);
			}
			else if (first.HasValue)
			{
				value2.FirstCommittedUtc = first.Value;
			}
			if (!string.IsNullOrEmpty(value2.NameKey) && _byName.ContainsKey(value2.NameKey) && _byName[value2.NameKey] == value2)
			{
				_byName.Remove(value2.NameKey);
			}
			value2.Name = name;
			value2.HeadSeq = seq;
			value2.HeadSha256 = sha;
			value2.HeadLength = length;
			value2.HeadCommittedUtc = committed;
			value2.NameKey = NameNormalizer.KeyOrNull(name) ?? "";
			_owner[id] = steam;
			if (value2.NameKey.Length > 0 && !_byName.ContainsKey(value2.NameKey))
			{
				_byName[value2.NameKey] = value2;
			}
		}

		public string CharacterDirectory(string steam64, long characterId)
		{
			return CharDir(steam64, characterId);
		}

		public bool HasCharacter(string steam64, long characterId)
		{
			lock (_gate)
			{
				return _fs.DirectoryExists(CharDir(steam64, characterId));
			}
		}

		public void DetachCharacter(string steam64, long characterId, string destination)
		{
			lock (_gate)
			{
				_fs.MoveDirectory(CharDir(steam64, characterId), destination);
				ForgetIndex(steam64, characterId);
			}
		}

		public void AttachCharacter(string steam64, long characterId, string source)
		{
			lock (_gate)
			{
				_fs.MoveDirectory(source, CharDir(steam64, characterId));
				string dir = CharDir(steam64, characterId);
				CleanupUncommitted(dir);
				List<int> seqs = CommittedSeqs(dir);
				HeadInfo headInfo = LoadHead(dir, seqs);
				if (headInfo != null)
				{
					UpdateIndex(steam64, characterId, headInfo.Name, headInfo.Seq, headInfo.Sha, headInfo.Length, headInfo.Committed);
				}
			}
		}

		private void ForgetIndex(string steam, long id)
		{
			if (_byAccount.TryGetValue(steam, out var value))
			{
				if (value.TryGetValue(id, out var value2))
				{
					if (!string.IsNullOrEmpty(value2.NameKey) && _byName.ContainsKey(value2.NameKey) && _byName[value2.NameKey] == value2)
					{
						_byName.Remove(value2.NameKey);
					}
					value.Remove(id);
				}
				if (value.Count == 0)
				{
					_byAccount.Remove(steam);
				}
			}
			_owner.Remove(id);
		}

		public RegisteredCharacter HeadOf(string steam64, long characterId)
		{
			lock (_gate)
			{
				Dictionary<long, RegisteredCharacter> value;
				RegisteredCharacter value2;
				return (_byAccount.TryGetValue(steam64, out value) && value.TryGetValue(characterId, out value2)) ? Clone(value2) : null;
			}
		}

		public List<KeyValuePair<string, long>> ListLiveCharacters()
		{
			lock (_gate)
			{
				List<KeyValuePair<string, long>> list = new List<KeyValuePair<string, long>>();
				string[] directories = _fs.GetDirectories(_root);
				foreach (string text in directories)
				{
					string fileName = Path.GetFileName(text);
					if (!IsValidSteam64(fileName))
					{
						continue;
					}
					string[] directories2 = _fs.GetDirectories(text);
					for (int j = 0; j < directories2.Length; j++)
					{
						if (long.TryParse(Path.GetFileName(directories2[j]), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result))
						{
							list.Add(new KeyValuePair<string, long>(fileName, result));
						}
					}
				}
				return list;
			}
		}

		public int Prune(string steam64, long characterId)
		{
			lock (_gate)
			{
				string dir = CharDir(steam64, characterId);
				List<int> list = CommittedSeqs(dir);
				if (list.Count <= 2)
				{
					return 0;
				}
				HashSet<int> keep = new HashSet<int>();
				keep.Add(list[list.Count - 1]);
				keep.Add(list[list.Count - 2]);
				foreach (int item in list.Skip(Math.Max(0, list.Count - 50)))
				{
					keep.Add(item);
				}
				DateTime dateTime = _utcNow().Date.AddDays(-30.0);
				Dictionary<DateTime, int> dictionary = new Dictionary<DateTime, int>();
				foreach (int item2 in list)
				{
					Dictionary<string, string> dictionary2 = ReadMeta(Stem(dir, item2) + ".meta");
					if (!dictionary2.ContainsKey("committedUtc") || !DateTime.TryParse(dictionary2["committedUtc"], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result))
					{
						keep.Add(item2);
					}
					else if (result.Date >= dateTime)
					{
						dictionary[result.Date] = item2;
					}
				}
				foreach (int value in dictionary.Values)
				{
					keep.Add(value);
				}
				int num = 0;
				foreach (int item3 in list.Where((int x) => !keep.Contains(x)))
				{
					string text = Stem(dir, item3);
					SafeDelete(text + ".ok");
					SafeDelete(text + ".meta");
					SafeDelete(text + ".fch");
					num++;
				}
				return num;
			}
		}
	}
}
namespace Beastheim.ServerMods.Characters.Server
{
	public sealed class ServerCharacterEngine
	{
		private readonly RevisionStore _store;

		private readonly Func<DateTime> _utcNow;

		private readonly Action<string> _log;

		private readonly FreshAllowList _allow = FreshAllowList.EvidenceDefault();

		private readonly Dictionary<object, ChunkReceiver> _receivers = new Dictionary<object, ChunkReceiver>();

		private readonly ICharacterTrace _t;

		private readonly Dictionary<object, double[]> _timing = new Dictionary<object, double[]>();

		public Func<object, int> ConnId = (object c) => 0;

		public CharacterServerMode Mode { get; private set; }

		public ServerCharacterEngine(RevisionStore store, CharacterServerMode mode, Action<string> log, Func<DateTime> utcNow = null, ICharacterTrace trace = null)
		{
			_t = trace ?? NullTrace.Instance;
			if (mode == CharacterServerMode.Enforce)
			{
				throw new NotSupportedException("Enforce mode is not available in this build; it needs the dedicated-server test gate first.");
			}
			_store = store;
			Mode = mode;
			_log = log ?? ((Action<string>)delegate
			{
			});
			_utcNow = utcNow ?? ((Func<DateTime>)(() => DateTime.UtcNow));
		}

		public OutMsg Caps()
		{
			return new OutMsg("Beastheim_ServerMods_CharCaps", CapsMessage.ForMode(Mode).ToBytes());
		}

		public IList<OutMsg> OnClaim(object conn, string steam64, byte[] data)
		{
			List<OutMsg> list = new List<OutMsg>();
			if (Mode == CharacterServerMode.Off)
			{
				return list;
			}
			if (!ClaimMessage.TryParse(data, out var m, out var error))
			{
				_log("[" + steam64 + "] claim rejected as malformed: " + error);
				return list;
			}
			_t.Event("claim.received", "conn", ConnId(conn), "steam", steam64, "characterId", m.CharacterId, "name", m.Name, "bytes", m.FileLength, "sha", TraceFormat.Short(m.Sha256), "lastAckedSeq", m.LastAckedSeq, "source", m.Source, "unreadable", m.Unreadable);
			ClaimVerdict claimVerdict = ClaimEvaluator.Evaluate(m, steam64, _store);
			_t.Event("claim.verdict", "conn", ConnId(conn), "steam", steam64, "code", claimVerdict.Code, "wouldReject", claimVerdict.WouldReject, "needBytes", claimVerdict.NeedBytes, "registeredCharacterId", (claimVerdict.Registered == null) ? 0 : claimVerdict.Registered.CharacterId, "headSeq", (claimVerdict.Registered != null) ? claimVerdict.Registered.HeadSeq : 0, "headSha", (claimVerdict.Registered == null) ? "none" : TraceFormat.Short(claimVerdict.Registered.HeadSha256), "admissionAffected", false);
			_log("[" + steam64 + "] claim: character " + m.CharacterId + " '" + m.Name + "', " + m.FileLength + " bytes -> " + claimVerdict.Code + (claimVerdict.WouldReject ? ("  OBSERVE: would REJECT admission (" + claimVerdict.PlayerMessage + ")") : "  OBSERVE: would PASS"));
			if (claimVerdict.Outcome == ClaimOutcome.Match)
			{
				_t.Event("claim.ack.sent", "conn", ConnId(conn), "steam", steam64, "seq", claimVerdict.Registered.HeadSeq, "sha", TraceFormat.Short(claimVerdict.Registered.HeadSha256));
			}
			if (claimVerdict.Outcome == ClaimOutcome.Match)
			{
				list.Add(new OutMsg("Beastheim_ServerMods_CharAck", new AckMessage
				{
					TransferId = 0L,
					Status = AckStatus.Unchanged,
					Seq = claimVerdict.Registered.HeadSeq,
					Sha256 = claimVerdict.Registered.HeadSha256,
					Text = "claim matches"
				}.ToBytes())
				{
					Conn = conn
				});
			}
			if (claimVerdict.NeedBytes)
			{
				_t.Event("need.sent", "conn", ConnId(conn), "steam", steam64, "reason", claimVerdict.NeedReason);
			}
			if (claimVerdict.NeedBytes)
			{
				list.Add(new OutMsg("Beastheim_ServerMods_CharNeed", new NeedMessage
				{
					Reason = claimVerdict.NeedReason
				}.ToBytes())
				{
					Conn = conn
				});
			}
			return list;
		}

		public IList<OutMsg> OnChunk(object conn, string steam64, byte[] data, double now)
		{
			List<OutMsg> list = new List<OutMsg>();
			if (Mode == CharacterServerMode.Off)
			{
				return list;
			}
			if (!ChunkMessage.TryParse(data, out var m, out var error))
			{
				_log("[" + steam64 + "] chunk rejected as malformed: " + error);
				if (_receivers.TryGetValue(conn, out var value))
				{
					long num = value.Abort();
					if (num != 0L)
					{
						list.Add(Ack(conn, num, AckStatus.Aborted, "malformed chunk: " + error));
					}
				}
				return list;
			}
			if (!_receivers.TryGetValue(conn, out var value2))
			{
				value2 = new ChunkReceiver();
				_receivers[conn] = value2;
			}
			if (!_timing.TryGetValue(conn, out var value3) || (long)value3[0] != m.TransferId)
			{
				value3 = new double[3] { m.TransferId, now, now };
				_timing[conn] = value3;
				_t.Event("transfer.begin", "conn", ConnId(conn), "steam", steam64, "transferId", m.TransferId, "kind", m.Kind, "characterId", m.CharacterId, "bytes", m.TotalLength, "chunks", m.Count, "declaredSha", TraceFormat.Short(m.Sha256), "parentSeq", m.ParentSeq);
			}
			_t.Event("chunk.received", "conn", ConnId(conn), "transferId", m.TransferId, "index", m.Index, "count", m.Count, "payloadBytes", m.Payload.Length, "gapMs", (m.Index == 0) ? (-1.0) : Math.Round((now - value3[2]) * 1000.0), "sinceFirstMs", Math.Round((now - value3[1]) * 1000.0));
			value3[2] = now;
			ChunkReceiver.Result result = value2.Feed(m, now);
			if (result.AbandonedTransferId != 0L)
			{
				_log("[" + steam64 + "] transfer " + result.AbandonedTransferId + " abandoned by the client before it completed; previous revision untouched");
				list.Add(Ack(conn, result.AbandonedTransferId, AckStatus.Aborted, "superseded before completion"));
			}
			if (result.Status == ChunkReceiver.Status.Error)
			{
				_log("[" + steam64 + "] transfer " + result.TransferId + " failed: " + result.Error + "; previous revision untouched");
				_t.Event("transfer.aborted", "conn", ConnId(conn), "transferId", result.TransferId, "reason", result.Error, "previousRevisionTouched", false);
				list.Add(Ack(conn, result.TransferId, AckStatus.Aborted, result.Error));
				return list;
			}
			if (result.Status == ChunkReceiver.Status.InProgress)
			{
				return list;
			}
			list.Add(Complete(conn, steam64, result, now));
			return list;
		}

		private OutMsg Complete(object conn, string steam64, ChunkReceiver.Result r, double now)
		{
			RegisteredCharacter registeredCharacter = null;
			foreach (RegisteredCharacter accountCharacter in _store.GetAccountCharacters(steam64))
			{
				if (accountCharacter.CharacterId == r.CharacterId)
				{
					registeredCharacter = accountCharacter;
				}
			}
			byte[] previousBytes = ((registeredCharacter != null) ? _store.ReadRevision(steam64, r.CharacterId, registeredCharacter.HeadSeq) : null);
			DateTime committedBefore = registeredCharacter?.HeadCommittedUtc ?? DateTime.MinValue;
			_t.Event("transfer.complete", "conn", ConnId(conn), "steam", steam64, "transferId", r.TransferId, "kind", r.Kind, "bytes", r.File.Length, "declaredSha", TraceFormat.Short(r.Sha256), "reassembledSha", TraceFormat.Short(Sha(r.File)), "shaMatch", true);
			_t.Event("commit.begin", "conn", ConnId(conn), "transferId", r.TransferId, "characterId", r.CharacterId);
			Stopwatch stopwatch = Stopwatch.StartNew();
			CommitResult commitResult = _store.Commit(steam64, r.CharacterId, r.File, r.ParentSeq, r.Kind);
			_t.Event("commit.done", "conn", ConnId(conn), "transferId", r.TransferId, "status", commitResult.Status, "seq", commitResult.Seq, "sha", TraceFormat.Short(commitResult.Sha256), "ms", stopwatch.ElapsedMilliseconds, "error", commitResult.Error);
			if (commitResult.Status == CommitStatus.Committed || commitResult.Status == CommitStatus.Unchanged)
			{
				_log("[" + steam64 + "] " + r.Kind.ToString() + " transfer " + r.TransferId + " committed as revision " + commitResult.Seq + ((commitResult.Status == CommitStatus.Unchanged) ? " (unchanged)" : "") + ", " + r.File.Length + " bytes, character " + r.CharacterId + " '" + ((commitResult.Parsed != null) ? commitResult.Parsed.Name : "") + "'");
				if (commitResult.Status == CommitStatus.Committed)
				{
					Observe(steam64, commitResult, registeredCharacter, previousBytes, committedBefore);
				}
				_t.Event("ack.sent", "conn", ConnId(conn), "transferId", r.TransferId, "status", (commitResult.Status == CommitStatus.Committed) ? AckStatus.Committed : AckStatus.Unchanged, "seq", commitResult.Seq, "sha", TraceFormat.Short(commitResult.Sha256), "afterCommit", true);
				return Ack(conn, r.TransferId, (commitResult.Status == CommitStatus.Committed) ? AckStatus.Committed : AckStatus.Unchanged, "", commitResult.Seq, commitResult.Sha256);
			}
			_log("[" + steam64 + "] transfer " + r.TransferId + " NOT stored (" + commitResult.Status.ToString() + "): " + commitResult.Error + "; previous revision untouched");
			_t.Event("ack.sent", "conn", ConnId(conn), "transferId", r.Tr