Decompiled source of RunicSentinelClient v1.0.5

plugins/RunicSentinelClient/RunicSentinelClient.dll

Decompiled 2 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Runic.Localization;
using RunicSentinel.Admission;
using RunicSentinel.Core;
using RunicSentinel.PlayerActions;
using RunicSentinel.Runtime;
using RunicSentinelClient.Runtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Runic Sentinel Client")]
[assembly: AssemblyDescription("Minimal client-only profile reporter for authoritative Runic Sentinel admission")]
[assembly: AssemblyCompany("Chazman")]
[assembly: AssemblyProduct("Runic Sentinel Client")]
[assembly: AssemblyFileVersion("1.0.5.0")]
[assembly: AssemblyInformationalVersion("1.0.5")]
[assembly: InternalsVisibleTo("RunicSentinelClient.Tests")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("1.0.5.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 Runic.Localization
{
	internal sealed class LanguageCatalog
	{
		private readonly Dictionary<string, string> _english;

		private Dictionary<string, string> _selected = new Dictionary<string, string>();

		private readonly Func<string, string> _read;

		private string _language;

		internal LanguageCatalog(string english, Func<string, string> read)
		{
			_english = Parse(english);
			_read = read;
		}

		internal static Dictionary<string, string> Parse(string json)
		{
			if (string.IsNullOrWhiteSpace(json))
			{
				return new Dictionary<string, string>();
			}
			if (json.Length > 2097152)
			{
				throw new InvalidDataException("Language file exceeds 2 MiB.");
			}
			DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), new DataContractJsonSerializerSettings
			{
				UseSimpleDictionaryFormat = true
			});
			using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json.TrimStart('\ufeff')));
			return ((Dictionary<string, string>)dataContractJsonSerializer.ReadObject(stream)) ?? new Dictionary<string, string>();
		}

		internal void Select(string language)
		{
			language = (string.IsNullOrWhiteSpace(language) ? "English" : language);
			if (string.Equals(language, _language, StringComparison.Ordinal))
			{
				return;
			}
			_language = language;
			_selected = new Dictionary<string, string>();
			if (language.IndexOfAny(new char[4] { '/', '\\', ':', '\0' }) >= 0 || language == "." || language == "..")
			{
				return;
			}
			try
			{
				_selected = Parse(_read(language));
			}
			catch (Exception)
			{
			}
		}

		internal string Get(string key)
		{
			_english.TryGetValue(key, out var value);
			if (_selected.TryGetValue(key, out var value2) && !string.IsNullOrWhiteSpace(value2) && (value == null || SameFields(value, value2)))
			{
				return value2;
			}
			return value ?? key;
		}

		internal string English(string key)
		{
			if (!_english.TryGetValue(key, out var value))
			{
				return key;
			}
			return value;
		}

		internal string TranslateEnglish(string text)
		{
			foreach (KeyValuePair<string, string> item in _english)
			{
				if (item.Value == text)
				{
					return Get(item.Key);
				}
			}
			return text;
		}

		internal string Format(string key, params object[] args)
		{
			_english.TryGetValue(key, out var value);
			string text = Get(key);
			if (value != null && !SameFields(value, text))
			{
				text = value;
			}
			try
			{
				return string.Format(CultureInfo.CurrentCulture, text, args);
			}
			catch (FormatException)
			{
				try
				{
					return string.Format(CultureInfo.CurrentCulture, value ?? key, args);
				}
				catch (FormatException)
				{
					return value ?? key;
				}
			}
		}

		private static bool SameFields(string a, string b)
		{
			try
			{
				return Fields(a).SetEquals(Fields(b));
			}
			catch (FormatException)
			{
				return false;
			}
		}

		private static HashSet<int> Fields(string text)
		{
			HashSet<int> hashSet = new HashSet<int>();
			for (int i = 0; i < text.Length; i++)
			{
				if (text[i] == '}')
				{
					if (i + 1 >= text.Length || text[i + 1] != '}')
					{
						throw new FormatException();
					}
					i++;
				}
				else
				{
					if (text[i] != '{')
					{
						continue;
					}
					if (i + 1 < text.Length && text[i + 1] == '{')
					{
						i++;
						continue;
					}
					int num = ++i;
					for (; i < text.Length && char.IsDigit(text[i]); i++)
					{
					}
					if (num == i || !int.TryParse(text.Substring(num, i - num), out var result))
					{
						throw new FormatException();
					}
					hashSet.Add(result);
					for (; i < text.Length && text[i] != '}'; i++)
					{
						if (text[i] == '{')
						{
							throw new FormatException();
						}
					}
					if (i == text.Length)
					{
						throw new FormatException();
					}
				}
			}
			return hashSet;
		}
	}
	internal static class RunicText
	{
		private static readonly Assembly Owner = typeof(RunicText).Assembly;

		private static readonly string Module = Owner.GetName().Name;

		private static readonly LanguageCatalog Catalog = Create();

		private static LanguageCatalog Create()
		{
			using Stream stream = Owner.GetManifestResourceStream("Runic.Localization.English.json");
			using StreamReader streamReader = ((stream == null) ? null : new StreamReader(stream));
			return new LanguageCatalog(streamReader?.ReadToEnd() ?? "{}", ReadLanguage);
		}

		private static string ReadLanguage(string language)
		{
			string path = Path.Combine(Path.Combine(Path.GetDirectoryName(Owner.Location), "Translations", Module), language + ".json");
			if (!File.Exists(path))
			{
				return null;
			}
			return File.ReadAllText(path);
		}

		private static void SelectLanguage()
		{
			string language = "English";
			try
			{
				Localization instance = Localization.instance;
				language = ((instance != null) ? instance.GetSelectedLanguage() : null) ?? "English";
			}
			catch
			{
			}
			Catalog.Select(language);
		}

		internal static string Get(string key)
		{
			SelectLanguage();
			return Catalog.Get(key);
		}

		internal static string English(string key)
		{
			return Catalog.English(key);
		}

		internal static string TranslateEnglish(string text)
		{
			SelectLanguage();
			return Catalog.TranslateEnglish(text);
		}

		internal static string Format(string key, params object[] args)
		{
			SelectLanguage();
			return Catalog.Format(key, args);
		}
	}
}
namespace RunicSentinel.PlayerActions
{
	internal static class PlayerActionProtocol
	{
		internal const string CapabilityRpc = "runic.sentinel.player.capability.v1";

		internal const string ActionRpc = "runic.sentinel.player.action.v1";

		internal const string ResultRpc = "runic.sentinel.player.result.v1";

		internal const int Schema = 2;

		internal const int MaximumBytes = 4096;

		internal static readonly string[] Actions = new string[12]
		{
			"raiseskill", "resetskill", "heal", "puke", "clearstatus", "adrenaline", "addstatus", "god", "ghost", "fly",
			"nocost", "modes"
		};

		internal static string[] SkillsList()
		{
			return (from n in Enum.GetNames(typeof(SkillType))
				where n != "None"
				select n).ToArray();
		}

		internal static string Validate(string action, string args)
		{
			args = args ?? "";
			if (!Actions.Contains(action) || args.Length > 160 || args.Any(char.IsControl))
			{
				throw new InvalidOperationException("Unsupported player action or invalid arguments.");
			}
			string[] parts = args.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			switch (action)
			{
			case "raiseskill":
			case "resetskill":
			{
				int num2 = ((!(action == "raiseskill")) ? 1 : 2);
				if (parts.Length != num2 || !SkillsList().Any((string s) => s.Equals(parts[0], StringComparison.OrdinalIgnoreCase)))
				{
					throw new InvalidOperationException("Choose a valid skill name.");
				}
				if (num2 == 2 && (!int.TryParse(parts[1], NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result3) || result3 < -100 || result3 > 100 || result3 == 0))
				{
					throw new InvalidOperationException("Skill change must be a nonzero whole number from -100 to 100.");
				}
				break;
			}
			case "adrenaline":
			{
				if (!int.TryParse(args, NumberStyles.None, CultureInfo.InvariantCulture, out var result2) || result2 < 0 || result2 > 100)
				{
					throw new InvalidOperationException("Adrenaline must be from 0 to 100.");
				}
				break;
			}
			case "god":
			case "ghost":
			case "fly":
			case "nocost":
				if (args != "on" && args != "off")
				{
					throw new InvalidOperationException("Choose On or Off.");
				}
				break;
			case "addstatus":
			{
				if (parts.Length < 1 || parts.Length > 3 || parts[0].Any((char c) => !char.IsLetterOrDigit(c) && c != '_' && c != '-' && c != '.'))
				{
					throw new InvalidOperationException("Choose a registered status effect name, optionally followed by duration and intensity.");
				}
				for (int num = 1; num < parts.Length; num++)
				{
					if (!float.TryParse(parts[num], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || float.IsNaN(result) || float.IsInfinity(result) || result < 0f)
					{
						throw new InvalidOperationException("Duration and intensity must be finite, nonnegative numbers.");
					}
				}
				break;
			}
			default:
				if (args.Length != 0)
				{
					throw new InvalidOperationException("This action takes no arguments.");
				}
				break;
			}
			return string.Join(" ", parts);
		}
	}
	internal sealed class PlayerActionReceiver : IDisposable
	{
		[HarmonyPatch(typeof(MessageHud), "ShowMessage")]
		private static class AdministratorNotificationPatch
		{
			internal static bool Prefix()
			{
				return _notificationDepth == 0;
			}
		}

		private sealed class Receipt
		{
			internal string Body;

			internal string Message;

			internal bool Success;

			internal long Expires;
		}

		[ThreadStatic]
		private static int _notificationDepth;

		private ZNet _network;

		private ZRpc _server;

		private float _nextHello;

		private static PlayerActionReceiver _active;

		private Player _modePlayer;

		private readonly Dictionary<string, bool> _modeOriginal = new Dictionary<string, bool>();

		private readonly HashSet<string> _grantedModes = new HashSet<string>();

		private readonly Dictionary<string, Receipt> _seen = new Dictionary<string, Receipt>();

		internal static void WithoutNotifications(Action action)
		{
			_notificationDepth++;
			try
			{
				action();
			}
			finally
			{
				_notificationDepth--;
			}
		}

		internal static bool Granted(string mode)
		{
			PlayerActionReceiver active = _active;
			if (active != null && active._server != null && (Object)(object)active._modePlayer != (Object)null && active._modePlayer == Player.m_localPlayer && active._network == ZNet.instance)
			{
				ZRpc server = active._server;
				ZNet instance = ZNet.instance;
				if (server == ((instance == null) ? null : instance.GetServerPeer()?.m_rpc))
				{
					return active._grantedModes.Contains(mode);
				}
			}
			return false;
		}

		private static bool Mode(Player p, string mode)
		{
			return mode switch
			{
				"fly" => ((Character)p).IsDebugFlying(), 
				"ghost" => ((Character)p).InGhostMode(), 
				"god" => ((Character)p).InGodMode(), 
				_ => p.NoCostCheat(), 
			};
		}

		private static void SetMode(Player p, string mode, bool value)
		{
			switch (mode)
			{
			case "god":
				p.SetGodMode(value);
				return;
			case "ghost":
				p.SetGhostMode(value);
				return;
			case "nocost":
				p.SetNoPlacementCost(value);
				return;
			}
			FieldInfo? field = typeof(Player).GetField("m_debugFly", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field == null)
			{
				throw new InvalidOperationException("Flight state is unavailable.");
			}
			if ((bool)field.GetValue(p) != value)
			{
				p.ToggleDebugFly();
			}
		}

		private void SetGrantedMode(Player p, string mode, bool value)
		{
			if (_modePlayer != p)
			{
				ClearModes();
				_modePlayer = p;
			}
			if (!_modeOriginal.ContainsKey(mode))
			{
				_modeOriginal[mode] = Mode(p, mode);
			}
			_active = this;
			_grantedModes.Add(mode);
			try
			{
				SetMode(p, mode, value);
			}
			catch
			{
				_grantedModes.Remove(mode);
				throw;
			}
			if (!value)
			{
				_grantedModes.Remove(mode);
			}
		}

		private void ClearModes()
		{
			if ((Object)(object)_modePlayer != (Object)null)
			{
				foreach (KeyValuePair<string, bool> pair in _modeOriginal)
				{
					try
					{
						WithoutNotifications(delegate
						{
							SetMode(_modePlayer, pair.Key, pair.Value);
						});
					}
					catch
					{
					}
				}
			}
			_modeOriginal.Clear();
			_grantedModes.Clear();
			_modePlayer = null;
			if (_active == this)
			{
				_active = null;
			}
		}

		private static string Modes(Player p)
		{
			return "God: " + (((Character)p).InGodMode() ? "On" : "Off") + " · Ghost: " + (((Character)p).InGhostMode() ? "On" : "Off") + " · Fly: " + (((Character)p).IsDebugFlying() ? "On" : "Off") + " · No-cost: " + (p.NoCostCheat() ? "On" : "Off");
		}

		private static FieldInfo EffectField(Type type, string name)
		{
			Type type2 = type;
			while (type2 != null)
			{
				FieldInfo field = type2.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field != null)
				{
					return field;
				}
				type2 = type2.BaseType;
			}
			return null;
		}

		private static void SetEffectField(object target, string name, float value)
		{
			FieldInfo fieldInfo = EffectField(target.GetType(), name);
			if (fieldInfo == null)
			{
				throw new InvalidOperationException("Status parameter is unavailable: " + name);
			}
			fieldInfo.SetValue(target, value);
		}

		internal void Tick()
		{
			//IL_016f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0176: Expected O, but got Unknown
			ZNet instance = ZNet.instance;
			ZNetPeer val = (((Object)(object)instance != (Object)null && !instance.IsServer()) ? instance.GetServerPeer() : null);
			ZRpc val2 = ((val != null && val.IsReady()) ? val.m_rpc : null);
			if (instance != _network || val2 != _server)
			{
				Dispose();
				_network = instance;
				_server = val2;
				_nextHello = 0f;
				if (val2 != null)
				{
					val2.Register<ZPackage>("runic.sentinel.player.action.v1", (Action<ZRpc, ZPackage>)Receive);
				}
			}
			if ((Object)(object)_modePlayer != (Object)null && _modePlayer != Player.m_localPlayer)
			{
				ClearModes();
			}
			long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			string[] array = (from p in _seen
				where p.Value.Expires < now
				select p.Key).ToArray();
			foreach (string key in array)
			{
				_seen.Remove(key);
			}
			if (_server != null && !((Object)(object)Player.m_localPlayer == (Object)null) && !(Time.realtimeSinceStartup < _nextHello))
			{
				_nextHello = Time.realtimeSinceStartup + 5f;
				ZPackage val3 = new ZPackage();
				val3.Write(2);
				val3.Write(Player.m_localPlayer.GetPlayerID());
				_server.Invoke("runic.sentinel.player.capability.v1", new object[1] { val3 });
			}
		}

		private void Receive(ZRpc rpc, ZPackage package)
		{
			if (_server == null || rpc != _server || _network != ZNet.instance || _network.IsServer() || _network.GetServerPeer()?.m_rpc != rpc || package == null || package.Size() > 4096)
			{
				return;
			}
			string text = "";
			try
			{
				text = package.ReadString();
				long num = package.ReadLong();
				long num2 = package.ReadLong();
				string action = package.ReadString();
				string args = package.ReadString();
				long num3 = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
				if (text.Length != 32 || !Guid.TryParseExact(text, "N", out var _) || package.GetPos() != package.Size() || num2 < num3 || num2 > num3 + 45)
				{
					throw new InvalidOperationException("Invalid or expired player action.");
				}
				string text2 = num + "\n" + num2 + "\n" + action + "\n" + args;
				if (_seen.TryGetValue(text, out var value))
				{
					if (value.Body != text2)
					{
						throw new InvalidOperationException("Reused action identity.");
					}
					Reply(text, value.Success, value.Message);
					return;
				}
				if (_seen.Count >= 128)
				{
					throw new InvalidOperationException("Player action capacity reached.");
				}
				Receipt receipt = new Receipt
				{
					Body = text2,
					Expires = num3 + 90,
					Message = "Action did not complete."
				};
				_seen.Add(text, receipt);
				try
				{
					args = PlayerActionProtocol.Validate(action, args);
					Player player = Player.m_localPlayer;
					if ((Object)(object)player == (Object)null || player.GetPlayerID() != num || ((Character)player).IsDead())
					{
						throw new InvalidOperationException("The selected character changed, is dead, or is not ready.");
					}
					if (action != "modes" && !Achievements.IsCheatedAtAll())
					{
						throw new InvalidOperationException("This player must explicitly run confirmcheats in their own F5 console before receiving cheat actions. Sentinel does not confirm it for them.");
					}
					WithoutNotifications(delegate
					{
						Apply(player, action, args);
					});
					receipt.Success = true;
					receipt.Message = ((action == "modes" || new string[4] { "god", "ghost", "fly", "nocost" }.Contains(action)) ? Modes(player) : (action + " completed on " + player.GetPlayerName() + "."));
				}
				catch (Exception ex)
				{
					receipt.Message = ex.Message;
				}
				Reply(text, receipt.Success, receipt.Message);
			}
			catch (Exception ex2)
			{
				if (text.Length == 32)
				{
					Reply(text, ok: false, ex2.Message);
				}
			}
		}

		private void Apply(Player player, string action, string args)
		{
			string[] parts = args.Split(' ');
			if (action == null)
			{
				return;
			}
			switch (action.Length)
			{
			case 5:
				switch (action[0])
				{
				default:
					return;
				case 'm':
					_ = action == "modes";
					return;
				case 'g':
					break;
				}
				if (!(action == "ghost"))
				{
					break;
				}
				goto IL_0181;
			case 3:
				switch (action[0])
				{
				default:
					return;
				case 'g':
					if (!(action == "god"))
					{
						return;
					}
					break;
				case 'f':
					if (!(action == "fly"))
					{
						return;
					}
					break;
				}
				goto IL_0181;
			case 10:
				switch (action[1])
				{
				case 'a':
					if (action == "raiseskill")
					{
						((Character)player).GetSkills().CheatRaiseSkill(parts[0], (float)int.Parse(parts[1], CultureInfo.InvariantCulture), true);
					}
					break;
				case 'e':
					if (action == "resetskill")
					{
						((Character)player).GetSkills().CheatResetSkill(parts[0]);
					}
					break;
				case 'd':
					if (action == "adrenaline")
					{
						((Character)player).AddAdrenaline((float)int.Parse(args, CultureInfo.InvariantCulture) - player.GetAdrenaline());
					}
					break;
				case 'b':
				case 'c':
					break;
				}
				break;
			case 4:
				switch (action[0])
				{
				case 'h':
					if (action == "heal")
					{
						((Character)player).Heal(((Character)player).GetMaxHealth(), true);
						((Character)player).AddStamina(((Character)player).GetMaxStamina());
						((Character)player).AddEitr(((Character)player).GetMaxEitr());
					}
					break;
				case 'p':
					if (action == "puke")
					{
						player.ClearFood();
					}
					break;
				}
				break;
			case 6:
				if (!(action == "nocost"))
				{
					break;
				}
				goto IL_0181;
			case 11:
				if (action == "clearstatus")
				{
					player.ClearHardDeath();
					((Character)player).GetSEMan().RemoveAllStatusEffects(false);
				}
				break;
			case 9:
			{
				if (!(action == "addstatus"))
				{
					break;
				}
				StatusEffect val = ((IEnumerable<StatusEffect>)ObjectDB.instance?.m_StatusEffects).FirstOrDefault((Func<StatusEffect, bool>)((StatusEffect e) => (Object)(object)e != (Object)null && ((Object)e).name.Equals(parts[0], StringComparison.OrdinalIgnoreCase)));
				if ((Object)(object)val == (Object)null)
				{
					throw new InvalidOperationException("Status effect is not installed on this player's client.");
				}
				if (parts.Length > 2 && !(val is SE_Shield) && !(val is SE_Burning) && !(val is SE_Poison))
				{
					throw new InvalidOperationException("This status effect has no intensity parameter.");
				}
				StatusEffect val2 = ((Character)player).GetSEMan().AddStatusEffect(StringExtensionMethods.GetStableHashCode(((Object)val).name), true, 0, 0f, (short)(-1));
				if ((Object)(object)val2 == (Object)null)
				{
					throw new InvalidOperationException("The player could not receive this status effect.");
				}
				if (parts.Length > 1)
				{
					val2.m_ttl = float.Parse(parts[1], CultureInfo.InvariantCulture);
				}
				if (parts.Length <= 2)
				{
					break;
				}
				float num = float.Parse(parts[2], CultureInfo.InvariantCulture);
				if (val2 is SE_Shield)
				{
					SetEffectField(val2, "m_absorbDamage", num);
				}
				SE_Burning val3 = (SE_Burning)(object)((val2 is SE_Burning) ? val2 : null);
				if (val3 != null)
				{
					bool flag = ((Object)val).name == "Burning";
					SetEffectField(val2, flag ? "m_fireDamageLeft" : "m_spiritDamageLeft", 0f);
					MethodInfo? method = ((object)val2).GetType().GetMethod(flag ? "AddFireDamage" : "AddSpiritDamage", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
					if (method == null)
					{
						throw new InvalidOperationException("Burning intensity is unavailable.");
					}
					method.Invoke(val3, new object[1] { num });
				}
				if (val2 is SE_Poison)
				{
					SetEffectField(val2, "m_damageLeft", num);
					FieldInfo fieldInfo = EffectField(((object)val2).GetType(), "m_damageInterval");
					if (fieldInfo == null)
					{
						throw new InvalidOperationException("Poison intensity is unavailable.");
					}
					SetEffectField(val2, "m_damagePerHit", num / Math.Max(0.001f, val2.m_ttl) * (float)fieldInfo.GetValue(val2));
				}
				break;
			}
			case 7:
			case 8:
				break;
				IL_0181:
				SetGrantedMode(player, action, args == "on");
				break;
			}
		}

		private void Reply(string id, bool ok, string text)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write(id);
			val.Write(ok);
			val.Write((text.Length > 600) ? text.Substring(0, 600) : text);
			ZRpc server = _server;
			if (server != null)
			{
				server.Invoke("runic.sentinel.player.result.v1", new object[1] { val });
			}
		}

		public void Dispose()
		{
			ClearModes();
			if (_server != null)
			{
				try
				{
					_server.Unregister("runic.sentinel.player.action.v1");
				}
				catch
				{
				}
			}
			_server = null;
			_network = null;
			_seen.Clear();
		}
	}
}
namespace RunicSentinel.Admission
{
	internal sealed class AdmissionPluginEvidence
	{
		internal string Id { get; }

		internal string Version { get; }

		internal string Sha256 { get; }

		internal AdmissionPluginEvidence(string id, string version, string sha256)
		{
			Id = AdmissionValidation.RequireAtom(id, 1, 128, "id");
			Version = AdmissionValidation.RequireAtom(version, 1, 64, "version");
			Sha256 = AdmissionValidation.RequireLowerHex(sha256, 64, "sha256");
		}
	}
	internal sealed class AdmissionClientProfile
	{
		internal long CapturedUnixSeconds { get; }

		internal string Digest { get; }

		internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; }

		internal AdmissionClientProfile(long capturedUnixSeconds, string digest, IList<AdmissionPluginEvidence> plugins)
		{
			if (capturedUnixSeconds < 0)
			{
				throw new ArgumentOutOfRangeException("capturedUnixSeconds");
			}
			CapturedUnixSeconds = capturedUnixSeconds;
			Digest = AdmissionValidation.RequireLowerHex(digest, 64, "digest");
			if (plugins == null)
			{
				throw new ArgumentNullException("plugins");
			}
			Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins));
		}
	}
	internal sealed class AdmissionChallenge
	{
		private readonly byte[] _nonce;

		internal string RequestId { get; }

		internal byte[] Nonce => (byte[])_nonce.Clone();

		internal long IssuedUnixSeconds { get; }

		internal long DeadlineUnixSeconds { get; }

		internal AdmissionChallenge(string requestId, byte[] nonce, long issuedUnixSeconds, long deadlineUnixSeconds)
		{
			RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId");
			if (nonce == null || nonce.Length != 32)
			{
				throw new ArgumentOutOfRangeException("nonce");
			}
			if (issuedUnixSeconds < 0 || deadlineUnixSeconds < issuedUnixSeconds)
			{
				throw new ArgumentOutOfRangeException("issuedUnixSeconds");
			}
			_nonce = (byte[])nonce.Clone();
			IssuedUnixSeconds = issuedUnixSeconds;
			DeadlineUnixSeconds = deadlineUnixSeconds;
		}
	}
	internal sealed class AdmissionReport
	{
		internal string RequestId { get; }

		internal string ClientVersion { get; }

		internal long CapturedUnixSeconds { get; }

		internal long IssuedUnixSeconds { get; }

		internal string ProfileDigest { get; }

		internal string NonceBinding { get; }

		internal IReadOnlyList<AdmissionPluginEvidence> Plugins { get; }

		internal AdmissionReport(string requestId, string clientVersion, long capturedUnixSeconds, long issuedUnixSeconds, string profileDigest, string nonceBinding, IList<AdmissionPluginEvidence> plugins)
		{
			RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId");
			ClientVersion = AdmissionValidation.RequireAtom(clientVersion, 1, 32, "clientVersion");
			if (capturedUnixSeconds < 0 || issuedUnixSeconds < 0)
			{
				throw new ArgumentOutOfRangeException("capturedUnixSeconds");
			}
			ProfileDigest = AdmissionValidation.RequireLowerHex(profileDigest, 64, "profileDigest");
			NonceBinding = AdmissionValidation.RequireLowerHex(nonceBinding, 64, "nonceBinding");
			if (plugins == null || plugins.Count > 512)
			{
				throw new ArgumentOutOfRangeException("plugins");
			}
			CapturedUnixSeconds = capturedUnixSeconds;
			IssuedUnixSeconds = issuedUnixSeconds;
			Plugins = new ReadOnlyCollection<AdmissionPluginEvidence>(new List<AdmissionPluginEvidence>(plugins));
		}
	}
	internal sealed class AdmissionDecisionMessage
	{
		internal string RequestId { get; }

		internal bool Accepted { get; }

		internal bool ResumeHandshake { get; }

		internal string ReasonCode { get; }

		internal long PolicySequence { get; }

		internal string PolicyProfile { get; }

		internal long IssuedUnixSeconds { get; }

		internal AdmissionDecisionMessage(string requestId, bool accepted, bool resumeHandshake, string reasonCode, long policySequence, string policyProfile, long issuedUnixSeconds)
		{
			RequestId = AdmissionValidation.RequireLowerHex(requestId, 32, "requestId");
			ReasonCode = AdmissionValidation.RequireReason(reasonCode, "reasonCode");
			if (policySequence < 0 || issuedUnixSeconds < 0)
			{
				throw new ArgumentOutOfRangeException("policySequence");
			}
			if (!string.IsNullOrEmpty(policyProfile))
			{
				AdmissionValidation.RequireAtom(policyProfile, 1, 64, "policyProfile");
			}
			if (resumeHandshake && !accepted)
			{
				throw new ArgumentException("Only an accepted decision may resume the native handshake.", "resumeHandshake");
			}
			Accepted = accepted;
			ResumeHandshake = resumeHandshake;
			PolicySequence = policySequence;
			PolicyProfile = policyProfile ?? string.Empty;
			IssuedUnixSeconds = issuedUnixSeconds;
		}
	}
	internal static class AdmissionValidation
	{
		internal static string RequireAtom(string value, int minimum, int maximum, string parameterName)
		{
			if (!IsAtom(value, minimum, maximum))
			{
				throw new ArgumentException("Value is not a canonical ASCII atom.", parameterName);
			}
			return value;
		}

		internal static bool IsAtom(string value, int minimum, int maximum)
		{
			if (value == null || value.Length < minimum || value.Length > maximum)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_')
				{
					return false;
				}
			}
			return true;
		}

		internal static string RequireLowerHex(string value, int length, string parameterName)
		{
			if (!IsLowerHex(value, length))
			{
				throw new ArgumentException("Value is not canonical lowercase hexadecimal.", parameterName);
			}
			return value;
		}

		internal static bool IsLowerHex(string value, int length)
		{
			if (value == null || value.Length != length)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < '0' || c > '9') && (c < 'a' || c > 'f'))
				{
					return false;
				}
			}
			return true;
		}

		internal static string RequireReason(string value, string parameterName)
		{
			if (!IsReason(value))
			{
				throw new ArgumentException("Reason code is not canonical.", parameterName);
			}
			return value;
		}

		internal static bool IsReason(string value)
		{
			if (value == null || value.Length < 1 || value.Length > 96)
			{
				return false;
			}
			foreach (char c in value)
			{
				if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '-' && c != '.')
				{
					return false;
				}
			}
			return true;
		}
	}
	internal static class AdmissionProfileCanonicalizer
	{
		internal const int MaximumCanonicalBytes = 262144;

		private const string Header = "RUNIC-SENTINEL-CLIENT-PROFILE/1\n";

		internal static bool TryCreate(IEnumerable<AdmissionPluginEvidence> source, long capturedUnixSeconds, out AdmissionClientProfile profile, out string failure)
		{
			profile = null;
			failure = string.Empty;
			if (source == null || capturedUnixSeconds < 0)
			{
				failure = "profile-missing";
				return false;
			}
			List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>();
			foreach (AdmissionPluginEvidence item in source)
			{
				if (item == null)
				{
					failure = "profile-entry-null";
					return false;
				}
				if (list.Count >= 512)
				{
					failure = "profile-plugin-cap";
					return false;
				}
				list.Add(item);
			}
			list.Sort((AdmissionPluginEvidence left, AdmissionPluginEvidence right) => string.CompareOrdinal(left.Id, right.Id));
			string a = null;
			StringBuilder stringBuilder = new StringBuilder("RUNIC-SENTINEL-CLIENT-PROFILE/1\n", Math.Min(32768, 64 + list.Count * 160));
			foreach (AdmissionPluginEvidence item2 in list)
			{
				if (string.Equals(a, item2.Id, StringComparison.Ordinal))
				{
					failure = "profile-plugin-duplicate";
					return false;
				}
				a = item2.Id;
				stringBuilder.Append(item2.Id).Append('|').Append(item2.Version)
					.Append('|')
					.Append(item2.Sha256)
					.Append('\n');
				if (stringBuilder.Length > 262144)
				{
					failure = "profile-size";
					return false;
				}
			}
			byte[] bytes = Encoding.UTF8.GetBytes(stringBuilder.ToString());
			if (bytes.Length > 262144)
			{
				failure = "profile-size";
				return false;
			}
			string digest;
			using (SHA256 sHA = SHA256.Create())
			{
				digest = Hex(sHA.ComputeHash(bytes));
			}
			profile = new AdmissionClientProfile(capturedUnixSeconds, digest, list);
			return true;
		}

		internal static string Hex(byte[] bytes)
		{
			if (bytes == null)
			{
				throw new ArgumentNullException("bytes");
			}
			StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2);
			for (int i = 0; i < bytes.Length; i++)
			{
				stringBuilder.Append(bytes[i].ToString("x2"));
			}
			return stringBuilder.ToString();
		}

		internal static bool FixedTimeEquals(string left, string right)
		{
			if (left == null || right == null)
			{
				return false;
			}
			int num = left.Length ^ right.Length;
			int num2 = Math.Max(left.Length, right.Length);
			for (int i = 0; i < num2; i++)
			{
				char c = ((i < left.Length) ? left[i] : '\0');
				char c2 = ((i < right.Length) ? right[i] : '\0');
				num |= c ^ c2;
			}
			return num == 0;
		}
	}
	internal enum AdmissionMessageKind : byte
	{
		Challenge = 1,
		Report,
		Decision
	}
	internal static class AdmissionProtocolV2
	{
		internal const string DirectRpcName = "chazman.RunicSentinel.Admission.v2";

		internal const int WireSchema = 2;

		internal const int MaximumPlugins = 512;

		internal const int MaximumFrameBytes = 262144;

		internal const int NonceBytes = 32;

		internal const int RequestIdHexLength = 32;

		internal const long MaximumClockSkewSeconds = 60L;

		internal const long MaximumMessageAgeSeconds = 300L;

		internal const long MaximumChallengeLifetimeSeconds = 120L;

		private const int Magic = 843141970;

		private const int Terminal = 843337285;

		private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

		internal static AdmissionChallenge CreateChallenge(long nowUnixSeconds, long lifetimeSeconds)
		{
			if (nowUnixSeconds < 0 || lifetimeSeconds < 1 || lifetimeSeconds > 120 || nowUnixSeconds > long.MaxValue - lifetimeSeconds)
			{
				throw new ArgumentOutOfRangeException("lifetimeSeconds");
			}
			byte[] array = new byte[32];
			byte[] array2 = new byte[16];
			using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create())
			{
				randomNumberGenerator.GetBytes(array);
				randomNumberGenerator.GetBytes(array2);
			}
			return new AdmissionChallenge(AdmissionProfileCanonicalizer.Hex(array2), array, nowUnixSeconds, nowUnixSeconds + lifetimeSeconds);
		}

		internal static bool IsChallengeCurrent(AdmissionChallenge challenge, long nowUnixSeconds, out string failure)
		{
			failure = string.Empty;
			if (challenge == null || nowUnixSeconds < 0)
			{
				failure = "challenge-missing";
				return false;
			}
			if (challenge.DeadlineUnixSeconds < challenge.IssuedUnixSeconds || challenge.DeadlineUnixSeconds - challenge.IssuedUnixSeconds > 120)
			{
				failure = "challenge-lifetime";
				return false;
			}
			if (challenge.IssuedUnixSeconds < nowUnixSeconds - 300 || challenge.IssuedUnixSeconds > nowUnixSeconds + 60 || nowUnixSeconds > challenge.DeadlineUnixSeconds)
			{
				failure = "challenge-stale";
				return false;
			}
			return true;
		}

		internal static AdmissionReport CreateReport(AdmissionChallenge challenge, AdmissionClientProfile profile, string clientVersion, long nowUnixSeconds)
		{
			if (!IsChallengeCurrent(challenge, nowUnixSeconds, out var failure))
			{
				throw new InvalidOperationException(failure);
			}
			if (profile == null)
			{
				throw new ArgumentNullException("profile");
			}
			if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding))
			{
				throw new InvalidOperationException("nonce-binding-failed");
			}
			return new AdmissionReport(challenge.RequestId, clientVersion, profile.CapturedUnixSeconds, nowUnixSeconds, profile.Digest, binding, new List<AdmissionPluginEvidence>(profile.Plugins));
		}

		internal static bool TryValidateReport(AdmissionChallenge challenge, AdmissionReport report, long receivedUnixSeconds, out AdmissionClientProfile profile, out string failure)
		{
			profile = null;
			failure = string.Empty;
			if (!IsChallengeCurrent(challenge, receivedUnixSeconds, out failure))
			{
				return false;
			}
			if (report == null || !AdmissionProfileCanonicalizer.FixedTimeEquals(challenge.RequestId, report.RequestId))
			{
				failure = "report-request-mismatch";
				return false;
			}
			if (report.IssuedUnixSeconds < receivedUnixSeconds - 300 || report.IssuedUnixSeconds > receivedUnixSeconds + 60 || report.CapturedUnixSeconds > receivedUnixSeconds + 60)
			{
				failure = "report-stale";
				return false;
			}
			if (!AdmissionProfileCanonicalizer.TryCreate(report.Plugins, report.CapturedUnixSeconds, out profile, out failure))
			{
				return false;
			}
			if (!AdmissionProfileCanonicalizer.FixedTimeEquals(profile.Digest, report.ProfileDigest))
			{
				profile = null;
				failure = "report-digest-mismatch";
				return false;
			}
			if (!TryComputeNonceBinding(challenge.RequestId, challenge.Nonce, profile.Digest, out var binding) || !AdmissionProfileCanonicalizer.FixedTimeEquals(binding, report.NonceBinding))
			{
				profile = null;
				failure = "report-binding-mismatch";
				return false;
			}
			return true;
		}

		internal static bool TryComputeNonceBinding(string requestId, byte[] nonce, string profileDigest, out string binding)
		{
			binding = string.Empty;
			if (!AdmissionValidation.IsLowerHex(requestId, 32) || nonce == null || nonce.Length != 32 || !AdmissionValidation.IsLowerHex(profileDigest, 64))
			{
				return false;
			}
			string s = "RUNIC-SENTINEL-ADMISSION-BINDING/2\n" + requestId + "\n" + AdmissionProfileCanonicalizer.Hex(nonce) + "\n" + profileDigest + "\n";
			using (SHA256 sHA = SHA256.Create())
			{
				binding = AdmissionProfileCanonicalizer.Hex(sHA.ComputeHash(Encoding.ASCII.GetBytes(s)));
			}
			return true;
		}

		internal static byte[] EncodeChallenge(AdmissionChallenge value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			return Encode(AdmissionMessageKind.Challenge, delegate(BinaryWriter writer)
			{
				WriteString(writer, value.RequestId, 32);
				byte[] nonce = value.Nonce;
				writer.Write(nonce.Length);
				writer.Write(nonce);
				writer.Write(value.IssuedUnixSeconds);
				writer.Write(value.DeadlineUnixSeconds);
			});
		}

		internal static byte[] EncodeReport(AdmissionReport value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			if (value.Plugins.Count > 512)
			{
				throw new ArgumentOutOfRangeException("value");
			}
			string previous = null;
			return Encode(AdmissionMessageKind.Report, delegate(BinaryWriter writer)
			{
				WriteString(writer, value.RequestId, 32);
				WriteString(writer, value.ClientVersion, 32);
				writer.Write(value.CapturedUnixSeconds);
				writer.Write(value.IssuedUnixSeconds);
				WriteString(writer, value.ProfileDigest, 64);
				WriteString(writer, value.NonceBinding, 64);
				writer.Write(value.Plugins.Count);
				foreach (AdmissionPluginEvidence plugin in value.Plugins)
				{
					if (plugin == null || (previous != null && string.CompareOrdinal(previous, plugin.Id) >= 0))
					{
						throw new InvalidDataException("Profile entries must be strictly ordered.");
					}
					previous = plugin.Id;
					WriteString(writer, plugin.Id, 128);
					WriteString(writer, plugin.Version, 64);
					WriteString(writer, plugin.Sha256, 64);
				}
			});
		}

		internal static byte[] EncodeDecision(AdmissionDecisionMessage value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}
			return Encode(AdmissionMessageKind.Decision, delegate(BinaryWriter writer)
			{
				WriteString(writer, value.RequestId, 32);
				writer.Write(value.Accepted ? ((byte)1) : ((byte)0));
				writer.Write(value.ResumeHandshake ? ((byte)1) : ((byte)0));
				WriteString(writer, value.ReasonCode, 96);
				writer.Write(value.PolicySequence);
				WriteString(writer, value.PolicyProfile, 64);
				writer.Write(value.IssuedUnixSeconds);
			});
		}

		internal static bool TryGetKind(byte[] bytes, out AdmissionMessageKind kind, out string failure)
		{
			kind = (AdmissionMessageKind)0;
			failure = string.Empty;
			try
			{
				using MemoryStream input = NewReadStream(bytes);
				using BinaryReader reader = new BinaryReader(input, StrictUtf8);
				if (!ReadHeader(reader, out kind, out failure))
				{
					return false;
				}
				return true;
			}
			catch
			{
				kind = (AdmissionMessageKind)0;
				failure = "frame-malformed";
				return false;
			}
		}

		internal static bool TryDecodeChallenge(byte[] bytes, out AdmissionChallenge value, out string failure)
		{
			value = null;
			failure = string.Empty;
			try
			{
				using MemoryStream memoryStream = NewReadStream(bytes);
				using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8);
				if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Challenge, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !AdmissionValidation.IsLowerHex(value2, 32))
				{
					failure = "challenge-shape";
					return false;
				}
				int num = binaryReader.ReadInt32();
				if (num != 32 || Remaining(memoryStream) < (long)num + 20L)
				{
					failure = "challenge-nonce";
					return false;
				}
				byte[] nonce = binaryReader.ReadBytes(num);
				long num2 = binaryReader.ReadInt64();
				long num3 = binaryReader.ReadInt64();
				if (num2 < 0 || num3 < num2 || num3 - num2 > 120 || !ReadTerminal(binaryReader, memoryStream))
				{
					failure = "challenge-shape";
					return false;
				}
				value = new AdmissionChallenge(value2, nonce, num2, num3);
				return true;
			}
			catch
			{
				value = null;
				failure = "challenge-malformed";
				return false;
			}
		}

		internal static bool TryDecodeReport(byte[] bytes, out AdmissionReport value, out string failure)
		{
			value = null;
			failure = string.Empty;
			try
			{
				using MemoryStream memoryStream = NewReadStream(bytes);
				using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8);
				if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Report, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2) || !TryReadString(binaryReader, memoryStream, 32, out var value3))
				{
					failure = "report-shape";
					return false;
				}
				long num = binaryReader.ReadInt64();
				long num2 = binaryReader.ReadInt64();
				if (!TryReadString(binaryReader, memoryStream, 64, out var value4) || !TryReadString(binaryReader, memoryStream, 64, out var value5))
				{
					failure = "report-shape";
					return false;
				}
				int num3 = binaryReader.ReadInt32();
				if (num3 < 0 || num3 > 512)
				{
					failure = "report-plugin-cap";
					return false;
				}
				List<AdmissionPluginEvidence> list = new List<AdmissionPluginEvidence>(num3);
				string text = null;
				for (int i = 0; i < num3; i++)
				{
					if (!TryReadString(binaryReader, memoryStream, 128, out var value6) || !TryReadString(binaryReader, memoryStream, 64, out var value7) || !TryReadString(binaryReader, memoryStream, 64, out var value8) || !AdmissionValidation.IsAtom(value6, 1, 128) || !AdmissionValidation.IsAtom(value7, 1, 64) || !AdmissionValidation.IsLowerHex(value8, 64) || (text != null && string.CompareOrdinal(text, value6) >= 0))
					{
						failure = "report-plugin-shape";
						return false;
					}
					text = value6;
					list.Add(new AdmissionPluginEvidence(value6, value7, value8));
				}
				if (!AdmissionValidation.IsLowerHex(value2, 32) || !AdmissionValidation.IsAtom(value3, 1, 32) || num < 0 || num2 < 0 || !AdmissionValidation.IsLowerHex(value4, 64) || !AdmissionValidation.IsLowerHex(value5, 64) || !ReadTerminal(binaryReader, memoryStream))
				{
					failure = "report-shape";
					return false;
				}
				value = new AdmissionReport(value2, value3, num, num2, value4, value5, list);
				return true;
			}
			catch
			{
				value = null;
				failure = "report-malformed";
				return false;
			}
		}

		internal static bool TryDecodeDecision(byte[] bytes, out AdmissionDecisionMessage value, out string failure)
		{
			value = null;
			failure = string.Empty;
			try
			{
				using MemoryStream memoryStream = NewReadStream(bytes);
				using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8);
				if (!ReadExpectedHeader(binaryReader, AdmissionMessageKind.Decision, out failure) || !TryReadString(binaryReader, memoryStream, 32, out var value2))
				{
					failure = "decision-shape";
					return false;
				}
				byte b = binaryReader.ReadByte();
				byte b2 = binaryReader.ReadByte();
				if (!TryReadString(binaryReader, memoryStream, 96, out var value3))
				{
					failure = "decision-shape";
					return false;
				}
				long num = binaryReader.ReadInt64();
				if (!TryReadString(binaryReader, memoryStream, 64, out var value4))
				{
					failure = "decision-shape";
					return false;
				}
				long num2 = binaryReader.ReadInt64();
				if (!AdmissionValidation.IsLowerHex(value2, 32) || b > 1 || b2 > 1 || (b2 == 1 && b != 1) || !AdmissionValidation.IsReason(value3) || num < 0 || num2 < 0 || (value4.Length > 0 && !AdmissionValidation.IsAtom(value4, 1, 64)) || !ReadTerminal(binaryReader, memoryStream))
				{
					failure = "decision-shape";
					return false;
				}
				value = new AdmissionDecisionMessage(value2, b == 1, b2 == 1, value3, num, value4, num2);
				return true;
			}
			catch
			{
				value = null;
				failure = "decision-malformed";
				return false;
			}
		}

		internal static bool IsDecisionFresh(AdmissionDecisionMessage value, long nowUnixSeconds)
		{
			if (value != null && nowUnixSeconds >= 0 && value.IssuedUnixSeconds >= nowUnixSeconds - 300)
			{
				return value.IssuedUnixSeconds <= nowUnixSeconds + 60;
			}
			return false;
		}

		private static byte[] Encode(AdmissionMessageKind kind, Action<BinaryWriter> body)
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8);
			binaryWriter.Write(843141970);
			binaryWriter.Write(2);
			binaryWriter.Write((byte)kind);
			body(binaryWriter);
			binaryWriter.Write(843337285);
			binaryWriter.Flush();
			if (memoryStream.Length <= 0 || memoryStream.Length > 262144)
			{
				throw new InvalidDataException("Admission frame exceeds its wire bound.");
			}
			return memoryStream.ToArray();
		}

		private static MemoryStream NewReadStream(byte[] bytes)
		{
			if (bytes == null || bytes.Length == 0 || bytes.Length > 262144)
			{
				throw new InvalidDataException("Admission frame size is invalid.");
			}
			return new MemoryStream(bytes, writable: false);
		}

		private static bool ReadHeader(BinaryReader reader, out AdmissionMessageKind kind, out string failure)
		{
			kind = (AdmissionMessageKind)0;
			failure = string.Empty;
			if (reader.ReadInt32() != 843141970 || reader.ReadInt32() != 2)
			{
				failure = "frame-header";
				return false;
			}
			kind = (AdmissionMessageKind)reader.ReadByte();
			if ((int)kind < 1 || (int)kind > 3)
			{
				kind = (AdmissionMessageKind)0;
				failure = "frame-kind";
				return false;
			}
			return true;
		}

		private static bool ReadExpectedHeader(BinaryReader reader, AdmissionMessageKind expected, out string failure)
		{
			if (!ReadHeader(reader, out var kind, out failure))
			{
				return false;
			}
			if (kind == expected)
			{
				return true;
			}
			failure = "frame-kind";
			return false;
		}

		private static void WriteString(BinaryWriter writer, string value, int maximumCharacters)
		{
			if (value == null || value.Length > maximumCharacters)
			{
				throw new InvalidDataException("Admission string exceeds its bound.");
			}
			byte[] bytes = StrictUtf8.GetBytes(value);
			if (bytes.Length > maximumCharacters)
			{
				throw new InvalidDataException("Admission string exceeds its byte bound.");
			}
			writer.Write(bytes.Length);
			writer.Write(bytes);
		}

		private static bool TryReadString(BinaryReader reader, MemoryStream stream, int maximumBytes, out string value)
		{
			value = string.Empty;
			int num = reader.ReadInt32();
			if (num < 0 || num > maximumBytes || Remaining(stream) < num)
			{
				return false;
			}
			byte[] array = reader.ReadBytes(num);
			if (array.Length != num)
			{
				return false;
			}
			value = StrictUtf8.GetString(array);
			return value.Length <= maximumBytes;
		}

		private static bool ReadTerminal(BinaryReader reader, MemoryStream stream)
		{
			if (Remaining(stream) == 4 && reader.ReadInt32() == 843337285)
			{
				return Remaining(stream) == 0;
			}
			return false;
		}

		private static long Remaining(MemoryStream stream)
		{
			return stream.Length - stream.Position;
		}
	}
}
namespace RunicSentinel.Runtime
{
	internal static class SentinelBuildingLimits
	{
		[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
		private static class ConnectionPatch
		{
			private static bool Prepare()
			{
				return ShouldInstall();
			}

			private static void Prefix(ZNet __instance, ZNetPeer peer)
			{
				if (__instance.IsServer())
				{
					return;
				}
				Reset();
				_serverRpc = peer.m_rpc;
				peer.m_rpc.Register<ZPackage>("runic.sentinel.building.settings.v1", (Action<ZRpc, ZPackage>)delegate(ZRpc rpc, ZPackage package)
				{
					//IL_0064: Unknown result type (might be due to invalid IL or missing references)
					//IL_006a: Expected O, but got Unknown
					if (rpc != _serverRpc || ZNet.instance != __instance || __instance.IsServer())
					{
						return;
					}
					try
					{
						if (package == null || package.Size() != 16)
						{
							throw new InvalidOperationException("Invalid building settings packet.");
						}
						_remote = BuildingLimits.Decode(package.GetArray());
						rpc.Invoke("runic.sentinel.building.ack.v1", new object[1] { (object)new ZPackage(_remote.Encode()) });
					}
					catch (Exception)
					{
						Reset();
						__instance.Disconnect(peer);
					}
				});
			}
		}

		[HarmonyPatch(typeof(ZNet), "Disconnect", new Type[] { typeof(ZNetPeer) })]
		private static class DisconnectPatch
		{
			private static bool Prepare()
			{
				return ShouldInstall();
			}

			private static void Prefix(ZNetPeer peer)
			{
				if (peer?.m_rpc != null)
				{
					Acknowledged.Remove(peer.m_rpc);
					if (peer.m_rpc == _serverRpc)
					{
						Reset();
					}
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "Awake")]
		private static class ResetPatch
		{
			private static bool Prepare()
			{
				return ShouldInstall();
			}

			private static void Prefix()
			{
				Reset();
			}
		}

		[HarmonyPatch(typeof(ZNet), "OnDestroy")]
		private static class DestroyPatch
		{
			private static bool Prepare()
			{
				return ShouldInstall();
			}

			private static void Postfix()
			{
				Reset();
			}
		}

		[HarmonyPatch]
		internal static class TerrainPatch
		{
			private static bool Prepare()
			{
				return ShouldInstall();
			}

			private static IEnumerable<MethodBase> TargetMethods()
			{
				yield return AccessTools.Method(typeof(TerrainComp), "LevelTerrain", (Type[])null, (Type[])null);
				yield return AccessTools.Method(typeof(TerrainComp), "RaiseTerrain", (Type[])null, (Type[])null);
				yield return AccessTools.Method(typeof(TerrainComp), "ApplyToHeightmap", (Type[])null, (Type[])null);
				yield return AccessTools.Method(typeof(Heightmap), "LevelTerrain", (Type[])null, (Type[])null);
				yield return AccessTools.Method(typeof(Heightmap), "AtMaxWorldLevelDepth", (Type[])null, (Type[])null);
			}

			internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod)
			{
				//IL_014e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0158: Expected O, but got Unknown
				List<CodeInstruction> list = instructions.ToList();
				bool flag = __originalMethod.Name == "AtMaxWorldLevelDepth";
				int num = 0;
				int num2 = 0;
				for (int i = 0; i < list.Count; i++)
				{
					bool flag2 = list[i].opcode == OpCodes.Ldc_R4;
					bool flag3 = list[i].opcode == OpCodes.Ldc_R8;
					if (!flag2 && !flag3)
					{
						continue;
					}
					double num3 = Convert.ToDouble(list[i].operand);
					string name;
					int num4;
					object obj;
					if (flag && flag2 && (float)num3 == 7.95f)
					{
						name = "DigThreshold";
						num++;
					}
					else
					{
						if (flag || num3 != -8.0)
						{
							if (flag || num3 != 8.0)
							{
								continue;
							}
							if (i + 1 < list.Count)
							{
								num4 = ((list[i + 1].opcode == OpCodes.Sub) ? 1 : 0);
								if (num4 != 0)
								{
									obj = "Dig";
									goto IL_00fc;
								}
							}
							else
							{
								num4 = 0;
							}
							obj = "Raise";
							goto IL_00fc;
						}
						name = "NegativeDig";
						num++;
					}
					goto IL_010a;
					IL_00fc:
					name = (string)obj;
					if (num4 != 0)
					{
						num++;
					}
					else
					{
						num2++;
					}
					goto IL_010a;
					IL_010a:
					list[i].opcode = OpCodes.Call;
					list[i].operand = typeof(SentinelBuildingLimits).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic);
					if (flag3)
					{
						list.Insert(++i, new CodeInstruction(OpCodes.Conv_R8, (object)null));
					}
				}
				if (num != 1 || num2 != ((!flag) ? 1 : 0))
				{
					throw new InvalidOperationException("Building limit patch does not match installed Valheim: " + __originalMethod.DeclaringType.Name + "." + __originalMethod.Name);
				}
				return list;
			}
		}

		[HarmonyPatch(typeof(WearNTear), "GetMaterialProperties")]
		private static class SupportPatch
		{
			private static bool Prepare()
			{
				return ShouldInstall();
			}

			private static void Postfix(ref float verticalLoss)
			{
				verticalLoss = Active.VerticalLoss(verticalLoss);
			}
		}

		private const string SettingsRpc = "runic.sentinel.building.settings.v1";

		private const string AckRpc = "runic.sentinel.building.ack.v1";

		private static BuildingLimits _local = BuildingLimits.Vanilla;

		private static BuildingLimits _remote = BuildingLimits.Vanilla;

		private static ZRpc _serverRpc;

		private static readonly HashSet<ZRpc> Acknowledged = new HashSet<ZRpc>();

		internal static BuildingLimits Active
		{
			get
			{
				if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer())
				{
					return _remote;
				}
				return _local;
			}
		}

		internal static bool ShouldInstall()
		{
			PropertyInfo propertyInfo = Type.GetType("RunicSentinel.Plugin, RunicSentinel", throwOnError: false)?.GetProperty("BuildingLimitsAvailable", BindingFlags.Static | BindingFlags.Public);
			if (!(propertyInfo == null))
			{
				return !(bool)propertyInfo.GetValue(null);
			}
			return true;
		}

		internal static void Reset()
		{
			_remote = BuildingLimits.Vanilla;
			_serverRpc = null;
			Acknowledged.Clear();
		}

		internal static float Raise()
		{
			return Active.RaiseHeight;
		}

		internal static float Dig()
		{
			return Active.DigDepth;
		}

		internal static float NegativeDig()
		{
			return 0f - Active.DigDepth;
		}

		internal static float DigThreshold()
		{
			return Active.DigDepth - 0.05f;
		}
	}
}
namespace RunicSentinel.Core
{
	internal sealed class BuildingLimits
	{
		internal static readonly BuildingLimits Vanilla = new BuildingLimits(1f, 8f, 8f);

		internal readonly float SupportMultiplier;

		internal readonly float RaiseHeight;

		internal readonly float DigDepth;

		internal bool IsVanilla
		{
			get
			{
				if (SupportMultiplier == 1f && RaiseHeight == 8f)
				{
					return DigDepth == 8f;
				}
				return false;
			}
		}

		internal BuildingLimits(float supportMultiplier, float raiseHeight, float digDepth)
		{
			if (!Valid(supportMultiplier, 1f, 10f) || !Valid(raiseHeight, 1f, 64f) || !Valid(digDepth, 1f, 64f))
			{
				throw new ArgumentOutOfRangeException("supportMultiplier", "Building limits must be finite and within their documented ranges.");
			}
			SupportMultiplier = supportMultiplier;
			RaiseHeight = raiseHeight;
			DigDepth = digDepth;
		}

		private static bool Valid(float value, float min, float max)
		{
			if (!float.IsNaN(value) && value >= min)
			{
				return value <= max;
			}
			return false;
		}

		internal float VerticalLoss(float vanilla)
		{
			return vanilla / SupportMultiplier;
		}

		internal byte[] Encode()
		{
			using MemoryStream memoryStream = new MemoryStream();
			using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
			binaryWriter.Write(1);
			binaryWriter.Write(SupportMultiplier);
			binaryWriter.Write(RaiseHeight);
			binaryWriter.Write(DigDepth);
			return memoryStream.ToArray();
		}

		internal static BuildingLimits Decode(byte[] bytes)
		{
			if (bytes == null || bytes.Length != 16)
			{
				throw new InvalidDataException("Invalid building settings length.");
			}
			using BinaryReader binaryReader = new BinaryReader(new MemoryStream(bytes, writable: false));
			if (binaryReader.ReadInt32() != 1)
			{
				throw new InvalidDataException("Unsupported building settings schema.");
			}
			return new BuildingLimits(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
		}
	}
}
namespace RunicSentinelClient
{
	internal static class ClientConfiguration
	{
		internal static ConfigEntry<bool> Enabled { get; private set; }

		internal static void Bind(ConfigFile config)
		{
			Enabled = config.Bind<bool>("General", "Enabled", true, RunicText.Get("text_c2c47dd903fc") + RunicText.Get("text_3fffa17a165a"));
		}
	}
	[BepInPlugin("chazman.RunicSentinelClient", "Runic Sentinel Client", "1.0.5")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "chazman.RunicSentinelClient";

		public const string Name = "Runic Sentinel Client";

		public const string Version = "1.0.5";

		private Harmony _harmony;

		private readonly PlayerActionReceiver _playerReceiver = new PlayerActionReceiver();

		internal static ClientAdmissionRuntime ActiveRuntime { get; private set; }

		private void Awake()
		{
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Expected O, but got Unknown
			ClientConfiguration.Bind(((BaseUnityPlugin)this).Config);
			ConfigEntry<bool> enabled = ClientConfiguration.Enabled;
			if (enabled != null && !enabled.Value)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client is disabled; no worker or RPC handler was created.");
				return;
			}
			if (Application.isBatchMode)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client is client-only and remains inert in batch mode.");
				return;
			}
			try
			{
				ActiveRuntime = new ClientAdmissionRuntime(delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)message);
				}, delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				});
				_harmony = new Harmony("chazman.RunicSentinelClient");
				_harmony.PatchAll(typeof(Plugin).Assembly);
				if (!ClientTransportPatches.IsInstalled("chazman.RunicSentinelClient"))
				{
					throw new InvalidOperationException("The pre-admission connection patch was not installed.");
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Sentinel Client v1.0.5 initialized. It activates only for a client connection and reports self-observed compatibility evidence to the server.");
			}
			catch (Exception ex)
			{
				Shutdown();
				((BaseUnityPlugin)this).Logger.LogError((object)("Runic Sentinel Client failed safely: " + ex.GetType().Name + "."));
			}
		}

		private static bool FullPlayerReceiverActive()
		{
			PropertyInfo propertyInfo = Type.GetType("RunicSentinel.Plugin, RunicSentinel", throwOnError: false)?.GetProperty("PlayerActionsAvailable", BindingFlags.Static | BindingFlags.Public);
			if (propertyInfo != null)
			{
				return (bool)propertyInfo.GetValue(null);
			}
			return false;
		}

		private void Update()
		{
			try
			{
				ActiveRuntime?.Tick();
				if (FullPlayerReceiverActive())
				{
					_playerReceiver.Dispose();
				}
				else if (ActiveRuntime != null)
				{
					_playerReceiver.Tick();
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Sentinel Client admission tick failed safely (" + ex.GetType().Name + ")."));
			}
		}

		private void OnDestroy()
		{
			Shutdown();
		}

		private void Shutdown()
		{
			_playerReceiver.Dispose();
			try
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch
			{
			}
			_harmony = null;
			SentinelBuildingLimits.Reset();
			try
			{
				ActiveRuntime?.Dispose();
			}
			catch
			{
			}
			ActiveRuntime = null;
		}
	}
}
namespace RunicSentinelClient.Runtime
{
	internal sealed class ClientAdmissionRuntime : IDisposable
	{
		private readonly Action<string> _information;

		private readonly Action<string> _warning;

		private ClientProfileCollector _profiles;

		private ClientAdmissionTransport _transport;

		private bool _disposed;

		internal ClientAdmissionRuntime(Action<string> information, Action<string> warning)
		{
			_information = information;
			_warning = warning;
		}

		internal void OnConnectionStarted(ZNet network, ZNetPeer peer)
		{
			if (!_disposed && !((Object)(object)network == (Object)null) && ZNet.instance == network && !network.IsServer() && peer != null && peer.m_server && peer.m_rpc != null)
			{
				EnsureStarted();
				_transport.AttachServerPeer(network, peer);
			}
		}

		internal void Tick()
		{
			if (!_disposed)
			{
				ZNet instance = ZNet.instance;
				if ((Object)(object)instance != (Object)null && instance.IsServer())
				{
					StopClientServices();
					return;
				}
				if ((Object)(object)instance == (Object)null)
				{
					StopClientServices();
					return;
				}
				EnsureStarted();
				_transport.Tick();
			}
		}

		public void Dispose()
		{
			if (!_disposed)
			{
				_disposed = true;
				StopClientServices();
			}
		}

		private void EnsureStarted()
		{
			if (_profiles == null)
			{
				_profiles = new ClientProfileCollector();
				_transport = new ClientAdmissionTransport(_profiles, _information, _warning);
				_profiles.Start();
				_information?.Invoke("Sentinel Client started a bounded local plugin profile.");
			}
		}

		private void StopClientServices()
		{
			try
			{
				_transport?.Dispose();
			}
			catch
			{
			}
			_transport = null;
			try
			{
				_profiles?.Dispose();
			}
			catch
			{
			}
			_profiles = null;
		}
	}
	internal sealed class ClientAdmissionTransport : IDisposable
	{
		internal const int MaximumNativeResumeAttempts = 3;

		internal const int MaximumTrackedPeers = 64;

		private static readonly FieldInfo RpcFunctionsField = typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic);

		private static readonly FieldInfo InviteSecretKeyField = typeof(ZNet).GetField("m_inviteSecretKey", BindingFlags.Static | BindingFlags.NonPublic);

		private readonly object _gate = new object();

		private readonly ClientProfileCollector _profiles;

		private readonly Action<string> _information;

		private readonly Action<string> _warning;

		private ZNet _network;

		private ZNetPeer _serverPeer;

		private ZRpc _serverRpc;

		private object _registeredHandler;

		private ZNet _blockedNetwork;

		private ZNetPeer _blockedPeer;

		private ZRpc _blockedRpc;

		private AdmissionChallenge _challenge;

		private string _sentRequestId = string.Empty;

		private string _completedRequestId = string.Empty;

		private string _reportedProfileFailureStatus = string.Empty;

		private string _nativeHandshakeSecret = string.Empty;

		private int _nativeResumeAttempts;

		private bool _nativeHandshakeResumed;

		private bool _disposed;

		internal ClientAdmissionTransport(ClientProfileCollector profiles, Action<string> information, Action<string> warning)
		{
			_profiles = profiles ?? throw new ArgumentNullException("profiles");
			_information = information;
			_warning = warning;
		}

		internal bool AttachServerPeer(ZNet network, ZNetPeer peer)
		{
			if ((Object)(object)network == (Object)null || ZNet.instance != network || network.IsServer() || peer == null || !peer.m_server || peer.m_rpc == null)
			{
				return false;
			}
			ZRpc rpc = peer.m_rpc;
			lock (_gate)
			{
				if (_disposed)
				{
					return false;
				}
				if (_network == network && _serverPeer == peer && _serverRpc == rpc)
				{
					return true;
				}
				if (_blockedNetwork == network && _blockedPeer == peer && _blockedRpc == rpc)
				{
					return false;
				}
				DetachLocked();
				if (!TryReadInviteSecretKey(out var value))
				{
					BlockLocked(network, peer, rpc);
					Notify(_warning, RunicText.Get("text_fa3bfcf34dd5"));
					return false;
				}
				int num = StableHash("chazman.RunicSentinel.Admission.v2");
				if (!TryGetFunctionMap(rpc, out var functions))
				{
					BlockLocked(network, peer, rpc);
					Notify(_warning, RunicText.Get("text_965172dfe6a8"));
					return false;
				}
				if (functions.Contains(num))
				{
					BlockLocked(network, peer, rpc);
					Notify(_warning, RunicText.Get("text_47ce59380df7"));
					return false;
				}
				try
				{
					rpc.Register<ZPackage>("chazman.RunicSentinel.Admission.v2", (Action<ZRpc, ZPackage>)ReceivePackage);
					object obj = functions[num];
					if (obj == null)
					{
						rpc.Unregister("chazman.RunicSentinel.Admission.v2");
						BlockLocked(network, peer, rpc);
						Notify(_warning, RunicText.Get("text_80678fbd62f4"));
						return false;
					}
					_network = network;
					_serverPeer = peer;
					_serverRpc = rpc;
					_registeredHandler = obj;
					_nativeHandshakeSecret = value;
					ResetExchangeLocked();
					Notify(_information, RunicText.Get("text_256802b36bf6"));
					return true;
				}
				catch (Exception ex)
				{
					try
					{
						if (functions.Contains(num))
						{
							rpc.Unregister("chazman.RunicSentinel.Admission.v2");
						}
					}
					catch
					{
					}
					BlockLocked(network, peer, rpc);
					Notify(_warning, RunicText.Get("text_f5547dddf418") + ex.GetType().Name + ").");
					return false;
				}
			}
		}

		internal void Tick()
		{
			//IL_033c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0342: Expected O, but got Unknown
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null || instance.IsServer())
			{
				ClearDisconnected();
				return;
			}
			ZNetPeer val;
			try
			{
				val = instance.GetServerPeer();
			}
			catch
			{
				val = null;
			}
			if (val?.m_rpc != null)
			{
				AttachServerPeer(instance, val);
			}
			ZRpc serverRpc;
			AdmissionChallenge challenge;
			lock (_gate)
			{
				if (_disposed)
				{
					return;
				}
				if (_serverRpc == null)
				{
					if ((Object)(object)_blockedNetwork != (Object)null && (_blockedNetwork != instance || !IsTrackedServerPeer(instance, _blockedPeer)))
					{
						ClearBlockedLocked();
					}
					return;
				}
				if (_network != instance || _serverPeer == null || !_serverPeer.m_server || _serverPeer.m_rpc != _serverRpc || !IsTrackedServerPeer(instance, _serverPeer))
				{
					DetachLocked();
					return;
				}
				if (_challenge == null || string.Equals(_sentRequestId, _challenge.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				serverRpc = _serverRpc;
				challenge = _challenge;
			}
			long nowUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			if (!AdmissionProtocolV2.IsChallengeCurrent(challenge, nowUnixSeconds, out var failure))
			{
				lock (_gate)
				{
					if (_challenge == challenge)
					{
						_challenge = null;
					}
				}
				Notify(_warning, RunicText.Get("text_3154b686bf87") + failure + ".");
				return;
			}
			if (!_profiles.TryGet(out var profile, out var status))
			{
				bool flag = false;
				lock (_gate)
				{
					if (!string.Equals(status, "capturing-profile", StringComparison.Ordinal) && !string.Equals(status, "not-started", StringComparison.Ordinal) && !string.Equals(_reportedProfileFailureStatus, status, StringComparison.Ordinal))
					{
						_reportedProfileFailureStatus = status;
						flag = true;
					}
				}
				if (flag)
				{
					Notify(_warning, RunicText.Get("text_4b41a28d971b") + status + ").");
				}
				return;
			}
			byte[] array;
			try
			{
				array = AdmissionProtocolV2.EncodeReport(AdmissionProtocolV2.CreateReport(challenge, profile, "1.0.5", nowUnixSeconds));
			}
			catch (Exception ex)
			{
				Notify(_warning, RunicText.Get("text_6bb69ccb1357") + ex.GetType().Name + ").");
				return;
			}
			lock (_gate)
			{
				if (_disposed || _serverRpc != serverRpc || _challenge != challenge || string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				_sentRequestId = challenge.RequestId;
			}
			try
			{
				if (!RpcConnected(serverRpc))
				{
					lock (_gate)
					{
						if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal))
						{
							_sentRequestId = string.Empty;
						}
						return;
					}
				}
				serverRpc.Invoke("chazman.RunicSentinel.Admission.v2", new object[1] { (object)new ZPackage(array) });
				Notify(_information, RunicText.Get("text_7e3fa6436397") + profile.Plugins.Count + RunicText.Get("text_24c65f4c73bd"));
			}
			catch (Exception ex2)
			{
				lock (_gate)
				{
					if (string.Equals(_sentRequestId, challenge.RequestId, StringComparison.Ordinal))
					{
						_sentRequestId = string.Empty;
					}
				}
				Notify(_warning, RunicText.Get("text_f441b5e60c09") + ex2.GetType().Name + ").");
			}
		}

		public void Dispose()
		{
			lock (_gate)
			{
				if (!_disposed)
				{
					_disposed = true;
					DetachLocked();
				}
			}
		}

		private void ReceivePackage(ZRpc rpc, ZPackage package)
		{
			if (rpc == null || package == null)
			{
				return;
			}
			lock (_gate)
			{
				if (_disposed || _serverRpc != rpc || (Object)(object)_network == (Object)null || ZNet.instance != _network || _network.IsServer() || _serverPeer == null || !_serverPeer.m_server || _serverPeer.m_rpc != rpc)
				{
					return;
				}
			}
			byte[] array;
			try
			{
				if (package.Size() <= 0 || package.Size() > 262144)
				{
					return;
				}
				array = package.GetArray();
			}
			catch
			{
				return;
			}
			if (AdmissionProtocolV2.TryGetKind(array, out var kind, out var _))
			{
				switch (kind)
				{
				case AdmissionMessageKind.Challenge:
					ReceiveChallenge(rpc, array);
					break;
				case AdmissionMessageKind.Decision:
					ReceiveDecision(rpc, array);
					break;
				}
			}
		}

		private void ReceiveChallenge(ZRpc rpc, byte[] bytes)
		{
			if (!AdmissionProtocolV2.TryDecodeChallenge(bytes, out var value, out var failure) || !AdmissionProtocolV2.IsChallengeCurrent(value, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), out failure))
			{
				Notify(_warning, RunicText.Get("text_3154b686bf87") + failure + ".");
				return;
			}
			bool flag;
			lock (_gate)
			{
				if (_disposed || _serverRpc != rpc || string.Equals(_completedRequestId, value.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				flag = _challenge == null || !string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal);
				_challenge = value;
				_sentRequestId = string.Empty;
			}
			if (flag)
			{
				Notify(_information, RunicText.Get("text_77a95578092e"));
			}
		}

		private void ReceiveDecision(ZRpc rpc, byte[] bytes)
		{
			if (!AdmissionProtocolV2.TryDecodeDecision(bytes, out var value, out var failure) || !AdmissionProtocolV2.IsDecisionFresh(value, DateTimeOffset.UtcNow.ToUnixTimeSeconds()))
			{
				Notify(_warning, RunicText.Get("text_862b290fb35b") + failure + ".");
				return;
			}
			bool flag = false;
			string text = string.Empty;
			lock (_gate)
			{
				if (_disposed || _serverRpc != rpc || _challenge == null || !string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal) || !string.Equals(_sentRequestId, value.RequestId, StringComparison.Ordinal) || string.Equals(_completedRequestId, value.RequestId, StringComparison.Ordinal))
				{
					return;
				}
				flag = value.Accepted && value.ResumeHandshake;
				if (flag)
				{
					if (_nativeHandshakeResumed || _nativeResumeAttempts >= 3)
					{
						return;
					}
					_nativeResumeAttempts++;
					text = _nativeHandshakeSecret;
				}
				else
				{
					_completedRequestId = value.RequestId;
				}
			}
			if (value.Accepted)
			{
				Notify(_information, RunicText.Get("text_cfad5f821987") + value.ReasonCode + ").");
			}
			else
			{
				Notify(_warning, RunicText.Get("text_2d3f4fc3053f") + value.ReasonCode + ").");
			}
			if (!flag)
			{
				return;
			}
			try
			{
				rpc.Invoke("ServerHandshake", new object[1] { text });
				lock (_gate)
				{
					if (!_disposed && _serverRpc == rpc && _challenge != null && string.Equals(_challenge.RequestId, value.RequestId, StringComparison.Ordinal))
					{
						_nativeHandshakeResumed = true;
						_completedRequestId = value.RequestId;
					}
				}
			}
			catch (Exception ex)
			{
				Notify(_warning, RunicText.Get("text_66337c067689") + ex.GetType().Name + ").");
			}
		}

		private void ClearDisconnected()
		{
			lock (_gate)
			{
				ClearDisconnectedLocked();
			}
		}

		private void ClearDisconnectedLocked()
		{
			if ((!((Object)(object)_network != (Object)null) || ZNet.instance != _network || _network.IsServer() || _serverPeer == null || !_serverPeer.m_server || _serverRpc == null || _serverPeer.m_rpc != _serverRpc || !IsTrackedServerPeer(_network, _serverPeer)) && (_serverRpc != null || !((Object)(object)_blockedNetwork != (Object)null) || ZNet.instance != _blockedNetwork || _blockedNetwork.IsServer() || !IsTrackedServerPeer(_blockedNetwork, _blockedPeer)))
			{
				DetachLocked();
			}
		}

		private void DetachLocked()
		{
			ZRpc serverRpc = _serverRpc;
			object registeredHandler = _registeredHandler;
			_network = null;
			_serverPeer = null;
			_serverRpc = null;
			_registeredHandler = null;
			_nativeHandshakeSecret = string.Empty;
			ClearBlockedLocked();
			ResetExchangeLocked();
			if (serverRpc == null || registeredHandler == null || !TryGetFunctionMap(serverRpc, out var functions))
			{
				return;
			}
			int num = StableHash("chazman.RunicSentinel.Admission.v2");
			try
			{
				if (functions[num] == registeredHandler)
				{
					serverRpc.Unregister("chazman.RunicSentinel.Admission.v2");
				}
			}
			catch
			{
			}
		}

		private void BlockLocked(ZNet network, ZNetPeer peer, ZRpc rpc)
		{
			_blockedNetwork = network;
			_blockedPeer = peer;
			_blockedRpc = rpc;
		}

		private void ClearBlockedLocked()
		{
			_blockedNetwork = null;
			_blockedPeer = null;
			_blockedRpc = null;
		}

		private static bool IsTrackedServerPeer(ZNet network, ZNetPeer expected)
		{
			if ((Object)(object)network == (Object)null || expected == null || !expected.m_server)
			{
				return false;
			}
			List<ZNetPeer> peers;
			try
			{
				peers = network.GetPeers();
			}
			catch
			{
				return false;
			}
			if (peers == null)
			{
				return false;
			}
			int num = 0;
			foreach (ZNetPeer item in peers)
			{
				if (num++ >= 64)
				{
					break;
				}
				if (item == expected)
				{
					return true;
				}
			}
			return false;
		}

		private static bool RpcConnected(ZRpc rpc)
		{
			try
			{
				return rpc != null && rpc.IsConnected();
			}
			catch
			{
				return false;
			}
		}

		private static bool TryReadInviteSecretKey(out string value)
		{
			value = string.Empty;
			if (InviteSecretKeyField == null || !InviteSecretKeyField.IsStatic || InviteSecretKeyField.FieldType != typeof(string))
			{
				return false;
			}
			try
			{
				value = (InviteSecretKeyField.GetValue(null) as string) ?? string.Empty;
				return value.Length <= 1024;
			}
			catch
			{
				value = string.Empty;
				return false;
			}
		}

		private static void Notify(Action<string> sink, string message)
		{
			try
			{
				sink?.Invoke(message);
			}
			catch
			{
			}
		}

		private static bool TryGetFunctionMap(ZRpc rpc, out IDictionary functions)
		{
			functions = null;
			if (rpc == null || RpcFunctionsField == null)
			{
				return false;
			}
			try
			{
				functions = RpcFunctionsField.GetValue(rpc) as IDictionary;
				return functions != null;
			}
			catch
			{
				functions = null;
				return false;
			}
		}

		private static int StableHash(string value)
		{
			int num = 5381;
			int num2 = num;
			for (int i = 0; i < value.Length && value[i] != 0; i += 2)
			{
				num = ((num << 5) + num) ^ value[i];
				if (i == value.Length - 1 || value[i + 1] == '\0')
				{
					break;
				}
				num2 = ((num2 << 5) + num2) ^ value[i + 1];
			}
			return num + num2 * 1566083941;
		}

		private void ResetExchangeLocked()
		{
			_challenge = null;
			_sentRequestId = string.Empty;
			_completedRequestId = string.Empty;
			_reportedProfileFailureStatus = string.Empty;
			_nativeResumeAttempts = 0;
			_nativeHandshakeResumed = false;
		}
	}
	internal sealed class ClientProfileCollector : IDisposable
	{
		private readonly object _gate = new object();

		private CancellationTokenSource _cancellation;

		private AdmissionClientProfile _profile;

		private string _status = "not-started";

		private bool _started;

		private bool _disposed;

		internal void Start()
		{
			CancellationToken token;
			lock (_gate)
			{
				if (_disposed)
				{
					throw new ObjectDisposedException("ClientProfileCollector");
				}
				if (_started)
				{
					return;
				}
				_started = true;
				_status = "capturing-profile";
				_cancellation = new CancellationTokenSource();
				token = _cancellation.Token;
			}
			ClientPluginFile[] files;
			try
			{
				files = CaptureLoadedPlugins();
			}
			catch (Exception ex)
			{
				Publish(null, "profile-capture-failed-" + ex.GetType().Name, token);
				return;
			}
			Task.Run(delegate
			{
				if (ClientProfileBuilder.TryBuild(files, DateTimeOffset.UtcNow.ToUnixTimeSeconds, token, out var profile, out var failure))
				{
					Publish(profile, "ready", token);
				}
				else if (!token.IsCancellationRequested)
				{
					Publish(null, failure, token);
				}
			}, token);
		}

		internal bool TryGet(out AdmissionClientProfile profile, out string status)
		{
			lock (_gate)
			{
				profile = _profile;
				status = _status;
				return profile != null;
			}
		}

		public void Dispose()
		{
			CancellationTokenSource cancellation;
			lock (_gate)
			{
				if (_disposed)
				{
					return;
				}
				_disposed = true;
				cancellation = _cancellation;
				_cancellation = null;
				_profile = null;
				_status = "disposed";
			}
			if (cancellation == null)
			{
				return;
			}
			try
			{
				cancellation.Cancel();
			}
			catch
			{
			}
			try
			{
				cancellation.Dispose();
			}
			catch
			{
			}
		}

		private void Publish(AdmissionClientProfile profile, string status, CancellationToken token)
		{
			lock (_gate)
			{
				if (!_disposed && !token.IsCancellationRequested)
				{
					_profile = profile;
					_status = (AdmissionValidation.IsReason(status) ? status : "profile-build-failed");
				}
			}
		}

		private static ClientPluginFile[] CaptureLoadedPlugins()
		{
			if (Chainloader.PluginInfos.Count > 512)
			{
				throw new InvalidDataException("Plugin cap exceeded.");
			}
			List<ClientPluginFile> list = new List<ClientPluginFile>(Chainloader.PluginInfos.Count);
			foreach (KeyValuePair<string, PluginInfo> item in Chainloader.PluginInfos.OrderBy<KeyValuePair<string, PluginInfo>, string>((KeyValuePair<string, PluginInfo> keyValuePair) => keyValuePair.Key, StringComparer.Ordinal))
			{
				if (list.Count >= 512)
				{
					throw new InvalidDataException("Plugin cap exceeded.");
				}
				PluginInfo value = item.Value;
				string text = ((value != null) ? value.Location : null);
				object obj;
				if (value == null)
				{
					obj = null;
				}
				else
				{
					BepInPlugin metadata = value.Metadata;
					obj = ((metadata != null) ? metadata.Version?.ToString() : null);
				}
				string version = (string)obj;
				list.Add(new ClientPluginFile(item.Key, version, Path.GetFullPath(text ?? string.Empty)));
			}
			return list.ToArray();
		}
	}
	internal sealed class ClientPluginFile
	{
		internal string Id { get; }

		internal string Version { get; }

		internal string Path { get; }

		internal ClientPluginFile(string id, string version, string path)
		{
			Id = AdmissionValidation.RequireAtom(id, 1, 128, "id");
			Version = AdmissionValidation.RequireAtom(version, 1, 64, "version");
			if (string.IsNullOrWhiteSpace(path))
			{
				throw new ArgumentException("Path is required.", "path");
			}
			Path = System.IO.Path.GetFullPath(path);
		}
	}
	internal static class ClientProfileBuilder
	{
		private sealed class FileHash
		{
			internal string Digest { get; }

			internal long Length { get; }

			internal long WriteTicks { get; }

			internal FileHash(string digest, long length, long writeTicks)
			{
				Digest = digest;
				Length = length;
				WriteTicks = writeTicks;
			}
		}

		internal const long MaximumPluginBytes = 536870912L;

		internal const long MaximumTotalPluginBytes = 4294967296L;

		private static readonly StringComparer PathComparer = (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);

		internal static bool TryBuild(IEnumerable<ClientPluginFile> source, Func<long> unixTime, CancellationToken cancellation, out AdmissionClientProfile profile, out string failure)
		{
			profile = null;
			failure = string.Empty;
			try
			{
				if (source == null || unixTime == null)
				{
					failure = "profile-input-missing";
					return false;
				}
				List<ClientPluginFile> list = new List<ClientPluginFile>();
				foreach (ClientPluginFile item in source)
				{
					if (item == null)
					{
						failure = "profile-entry-null";
						return false;
					}
					if (list.Count >= 512)
					{
						failure = "profile-plugin-cap";
						return false;
					}
					list.Add(item);
				}
				List<AdmissionPluginEvidence> list2 = new List<AdmissionPluginEvidence>(list.Count);
				Dictionary<string, FileHash> dictionary = new Dictionary<string, FileHash>(PathComparer);
				byte[] buffer = new byte[65536];
				long num = 0L;
				foreach (ClientPluginFile item2 in list)
				{
					cancellation.ThrowIfCancellationRequested();
					string fullPath = Path.GetFullPath(item2.Path);
					if (!dictionary.TryGetValue(fullPath, out var value))
					{
						FileInfo fileInfo = new FileInfo(fullPath);
						if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > 536870912 || num > 4294967296L - fileInfo.Length)
						{
							failure = "profile-file-bound";
							return false;
						}
						long length = fileInfo.Length;
						long ticks = fileInfo.LastWriteTimeUtc.Ticks;
						num += length;
						string digest = HashExact(fullPath, length, buffer, cancellation);
						fileInfo.Refresh();
						if (!fileInfo.Exists || fileInfo.Length != length || fileInfo.LastWriteTimeUtc.Ticks != ticks)
						{
							failure = "profile-file-changed";
							return false;
						}
						value = new FileHash(digest, length, ticks);
						dictionary.Add(fullPath, value);
					}
					list2.Add(new AdmissionPluginEvidence(item2.Id, item2.Version, value.Digest));
				}
				long num2 = unixTime();
				if (num2 < 0)
				{
					failure = "profile-clock";
					return false;
				}
				return AdmissionProfileCanonicalizer.TryCreate(list2, num2, out profile, out failure);
			}
			catch (OperationCanceledException)
			{
				failure = "profile-cancelled";
				return false;
			}
			catch (Exception)
			{
				failure = "profile-build-failed";
				return false;
			}
		}

		private static string HashExact(string path, long expectedLength, byte[] buffer, CancellationToken cancellation)
		{
			using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, buffer.Length, FileOptions.SequentialScan);
			using SHA256 sHA = SHA256.Create();
			if (fileStream.Length != expectedLength)
			{
				throw new IOException("Plugin changed before hashing.");
			}
			long num = expectedLength;
			while (num > 0)
			{
				cancellation.ThrowIfCancellationRequested();
				int count = (int)Math.Min(buffer.Length, num);
				int num2 = fileStream.Read(buffer, 0, count);
				if (num2 <= 0)
				{
					throw new EndOfStreamException("Plugin ended during hashing.");
				}
				sHA.TransformBlock(buffer, 0, num2, buffer, 0);
				num -= num2;
			}
			if (fileStream.ReadByte() != -1 || fileStream.Length != expectedLength)
			{
				throw new IOException("Plugin grew during hashing.");
			}
			sHA.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
			return AdmissionProfileCanonicalizer.Hex(sHA.Hash);
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection", new Type[] { typeof(ZNetPeer) })]
	internal static class ClientTransportPatches
	{
		[HarmonyPrefix]
		[HarmonyPriority(800)]
		private static void ZNetOnNewConnectionPrefix(ZNet __instance, [HarmonyArgument(0)] ZNetPeer peer)
		{
			try
			{
				Plugin.ActiveRuntime?.OnConnectionStarted(__instance, peer);
			}
			catch
			{
			}
		}

		internal static bool IsInstalled(string owner)
		{
			if (string.IsNullOrEmpty(owner))
			{
				return false;
			}
			MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "OnNewConnection", new Type[1] { typeof(ZNetPeer) }, (Type[])null);
			return ((methodInfo == null) ? null : Harmony.GetPatchInfo((MethodBase)methodInfo))?.Prefixes.Any((Patch patch) => string.Equals(patch.owner, owner, StringComparison.Ordinal) && patch.priority == 800 && patch.PatchMethod?.DeclaringType == typeof(ClientTransportPatches)) ?? false;
		}
	}
}