Decompiled source of Valheim Chat Plus v0.1.17

BepInEx/plugins/ValheimChatPlus/ValheimChatPlus.dll

Decompiled 3 weeks ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("ValheimChatPlus")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Global chat, whispers, pings and player notifications for Valheim.")]
[assembly: AssemblyFileVersion("0.1.16.0")]
[assembly: AssemblyInformationalVersion("0.1.16")]
[assembly: AssemblyProduct("Valheim Chat Plus")]
[assembly: AssemblyTitle("ValheimChatPlus")]
[assembly: AssemblyVersion("0.1.16.0")]
namespace CrossplayDirectChat;

[BepInPlugin("local.crossplaydirectchat", "Valheim Chat Plus", "0.1.16")]
public sealed class Plugin : BaseUnityPlugin
{
	private sealed class Link
	{
		public object Peer;

		public object Rpc;

		public bool Ready;

		public bool Warned;

		public bool Local;

		public string Color;

		public string Identity;

		public string PresenceName;

		public long OwnPingUid;

		public string OwnPingName;

		public Rate ControlRate = new Rate();

		public PingRate Pings = new PingRate();

		public PingRate OutgoingPings = new PingRate();

		public float NextHello;

		public Rate Rate = new Rate();

		public Rate EventRate = new Rate();

		public bool Sleeping;

		public float LastDeathAt;
	}

	private sealed class PingLabel
	{
		internal string Name;
	}

	private const string Hello = "CDC0110_Hello";

	private const string Submit = "CDC0110_Submit";

	private const string Deliver = "CDC0110_Deliver";

	private const string Control = "CDC0110_Control";

	private const string Notice = "CDC0110_Notice";

	private static Plugin self;

	private Type netType;

	private Type chatType;

	private Type rpcType;

	private Harmony harmony;

	private FieldInfo hideTimer;

	private ConfigEntry<bool> defaultGlobal;

	private bool fallbackNotified;

	private object currentNet;

	private float nextScan;

	private readonly Dictionary<object, Link> links = new Dictionary<object, Link>();

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

	private ColorStore colorStore;

	private bool colorStoreFailed;

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

	private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

	private Link hostLink;

	private object hostPlayer;

	private MethodInfo hostUid;

	private MethodInfo hostPlayerId;

	private MethodInfo hostPlayerName;

	private MethodInfo hostIsDedicated;

	private bool hostWarning;

	private float nextHostAttempt;

	private const string PingSubmit = "CDC0110_PingSubmit";

	private const string PingDeliver = "CDC0110_PingDeliver";

	private Harmony pingHarmony;

	private Type pingUserType;

	private Type pingMapType;

	private FieldInfo pingLocalPlayer;

	private FieldInfo pingUserName;

	private FieldInfo pingMapMode;

	private FieldInfo pingNoMapDistance;

	private MethodInfo pingInIntro;

	private MethodInfo pingAddWorldText;

	private object pingType;

	private object pingNoMap;

	private bool pingsReady;

	private float nextPingError;

	private readonly ConditionalWeakTable<object, PingLabel> pingNames = new ConditionalWeakTable<object, PingLabel>();

	private const string WhisperSubmit = "CDC0110_WhisperSubmit";

	private const string WhisperDeliver = "CDC0110_WhisperDeliver";

	private const string WhisperRoster = "CDC0110_WhisperRoster";

	private const string WhisperNotice = "CDC0110_WhisperNotice";

	private WhisperPlayer[] roster = Array.Empty<WhisperPlayer>();

	private readonly WhisperCompletion completion = new WhisperCompletion();

	private readonly ReplyState reply = new ReplyState();

	private bool rosterDirty;

	private bool tabReady;

	private Harmony tabHarmony;

	private ConfigEntry<bool> whisperTab;

	private Func<bool> tabPressed;

	private Func<object, bool> inputFocused;

	private Func<object, bool> inputComposing;

	private Func<object, string> inputText;

	private Func<object, int> inputCaret;

	private Func<object, int> inputAnchor;

	private Func<object, int> inputFocus;

	private Action<object, string> replaceInput;

	private Action<object, int> replaceCaret;

	private Type localizationType;

	private MethodInfo selectedLanguage;

	private const string Presence = "CDC0110_Presence";

	private const string EventSubmit = "CDC0112_EventSubmit";

	private const string EventDeliver = "CDC0112_EventDeliver";

	private Harmony eventHarmony;

	private Type playerType;

	private Type characterType;

	private Type zdoType;

	private Type zdoVarsType;

	private Type znetViewType;

	private FieldInfo localPlayer;

	private FieldInfo lastHit;

	private FieldInfo netView;

	private FieldInfo zdoUid;

	private FieldInfo inBedField;

	private FieldInfo characterName;

	private MethodInfo localize;

	private MethodInfo getZdo;

	private int inBedHash;

	private bool sleepPatchReady;

	private bool sleepIntentPatchReady;

	private bool sleepStatePatchReady;

	private bool deathPatchReady;

	private bool localInBedState;

	private ConfigEntry<bool> messageSoundEnabled;

	private ConfigEntry<float> messageSoundVolume;

	private readonly MessageSoundGate soundGate = new MessageSoundGate();

	private bool soundAttempted;

	private Object messageClip;

	private Object messageSource;

	private Action<float> playMessageClip;

	private Func<bool> messageClipPlaying;

	private Func<float> gameSfxVolume;

	private bool Server
	{
		get
		{
			if (currentNet != null)
			{
				return (bool)Call(currentNet, "IsServer");
			}
			return false;
		}
	}

	private static object Get(object o, string n)
	{
		return AccessTools.Field(o.GetType(), n).GetValue(o);
	}

	private static object Instance(Type t)
	{
		object obj = AccessTools.Property(t, "instance")?.GetValue(null);
		if (obj == null)
		{
			FieldInfo fieldInfo = AccessTools.Field(t, "m_instance");
			if ((object)fieldInfo == null)
			{
				return null;
			}
			obj = fieldInfo.GetValue(null);
		}
		return obj;
	}

	private static object Call(object o, string n, params object[] args)
	{
		MethodInfo methodInfo = AccessTools.Method(o.GetType(), n, args.Select((object x) => x.GetType()).ToArray(), (Type[])null);
		if (methodInfo == null)
		{
			throw new MissingMethodException(o.GetType().FullName, n);
		}
		return methodInfo.Invoke(o, args);
	}

	private bool Active(Link l)
	{
		if (links.TryGetValue(l.Rpc, out var value) && value == l)
		{
			if (!l.Local)
			{
				return (bool)Call(l.Peer, "IsReady");
			}
			if (l == hostLink)
			{
				return Server;
			}
			return false;
		}
		return false;
	}

	private bool ServerLink(Link l)
	{
		if (!l.Local)
		{
			if (currentNet != null)
			{
				return Call(currentNet, "GetServerPeer") == l.Peer;
			}
			return false;
		}
		if (l == hostLink)
		{
			return Server;
		}
		return false;
	}

	private void Awake()
	{
		//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e7: Expected O, but got Unknown
		//IL_010f: Unknown result type (might be due to invalid IL or missing references)
		//IL_011d: Expected O, but got Unknown
		try
		{
			netType = AccessTools.TypeByName("ZNet");
			chatType = AccessTools.TypeByName("Chat");
			rpcType = AccessTools.TypeByName("ZRpc");
			if (netType == null || chatType == null || rpcType == null)
			{
				throw new Exception("Valheim types missing");
			}
			hideTimer = AccessTools.Field(chatType, "m_hideTimer");
			if (hideTimer == null || hideTimer.FieldType != typeof(float))
			{
				throw new MissingFieldException("Chat.m_hideTimer");
			}
			defaultGlobal = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "DefaultGlobal", true, "Prefer Valheim Chat Plus global chat for ordinary text. Uses vanilla without a compatible server. Toggle with /chatmode; saved across restarts.");
			self = this;
			harmony = new Harmony("local.crossplaydirectchat");
			harmony.Patch((MethodBase)AccessTools.Method(chatType, "InputText", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "Input", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			SetupLocalization();
			SetupWhispers();
			ConfigureMessageSound();
			SetupPings();
			SetupPlayerEvents();
			SetupHosting();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"VALHEIM_CHAT_PLUS_READY 0.1.16 — /chathelp; host or dedicated server + clients required");
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)ex);
			Harmony obj = harmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			((Behaviour)this).enabled = false;
		}
	}

	private void Register(Link l, string name, Action<string> action)
	{
		MethodInfo methodInfo = rpcType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Single((MethodInfo m) => m.Name == "Register" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1).MakeGenericMethod(typeof(string));
		Type parameterType = methodInfo.GetParameters()[1].ParameterType;
		ParameterExpression[] array = (from p in parameterType.GetMethod("Invoke").GetParameters()
			select Expression.Parameter(p.ParameterType)).ToArray();
		Action<string> value = delegate(string s)
		{
			try
			{
				if (Active(l))
				{
					action(s);
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_HANDLER: " + ex.GetType().Name));
			}
		};
		Delegate obj = Expression.Lambda(parameterType, Expression.Invoke(Expression.Constant(value), array[1]), array).Compile();
		methodInfo.Invoke(l.Rpc, new object[2] { name, obj });
	}

	private void Send(Link l, string name, string value)
	{
		if (l.Local)
		{
			DispatchHost(l, name, value);
			return;
		}
		Call(l.Rpc, "Invoke", name, new object[1] { value });
	}

	private void Update()
	{
		if (Time.realtimeSinceStartup < nextScan)
		{
			return;
		}
		nextScan = Time.realtimeSinceStartup + 0.5f;
		try
		{
			object obj = Instance(netType);
			if (obj != currentNet)
			{
				links.Clear();
				ResetHost();
				muted.Clear();
				names.Clear();
				ResetWhispers();
				colorStore = null;
				colorStoreFailed = false;
				localInBedState = false;
				fallbackNotified = false;
				currentNet = obj;
			}
			if (obj == null)
			{
				return;
			}
			EnsureHost();
			object[] peers = ((IEnumerable)Call(obj, "GetPeers")).Cast<object>().ToArray();
			object[] array = (from p in links
				where !p.Value.Local && !peers.Any((object x) => x == p.Value.Peer)
				select p.Key).ToArray();
			foreach (object key in array)
			{
				Link l = links[key];
				links.Remove(key);
				RosterChanged();
				AnnounceLeave(l);
			}
			array = peers;
			foreach (object obj2 in array)
			{
				if (!(bool)Call(obj2, "IsReady"))
				{
					continue;
				}
				object obj3 = Get(obj2, "m_rpc");
				if (!links.TryGetValue(obj3, out var l2))
				{
					l2 = new Link
					{
						Peer = obj2,
						Rpc = obj3
					};
					links.Add(obj3, l2);
					Register(l2, "CDC0110_Hello", delegate(string s)
					{
						OnHello(l2, s);
					});
					Register(l2, "CDC0110_Submit", delegate(string s)
					{
						OnSubmit(l2, s);
					});
					Register(l2, "CDC0110_Deliver", delegate(string s)
					{
						OnDeliver(l2, s);
					});
					Register(l2, "CDC0110_Control", delegate(string s)
					{
						OnControl(l2, s);
					});
					Register(l2, "CDC0110_Notice", delegate(string s)
					{
						OnNotice(l2, s);
					});
					Register(l2, "CDC0110_PingSubmit", delegate(string s)
					{
						OnPingSubmit(l2, s);
					});
					Register(l2, "CDC0110_PingDeliver", delegate(string s)
					{
						OnPingDeliver(l2, s);
					});
					Register(l2, "CDC0110_WhisperSubmit", delegate(string s)
					{
						OnWhisperSubmit(l2, s);
					});
					Register(l2, "CDC0110_WhisperDeliver", delegate(string s)
					{
						OnWhisperDeliver(l2, s);
					});
					Register(l2, "CDC0110_WhisperRoster", delegate(string s)
					{
						OnWhisperRoster(l2, s);
					});
					Register(l2, "CDC0110_WhisperNotice", delegate(string s)
					{
						OnWhisperNotice(l2, s);
					});
					Register(l2, "CDC0110_Presence", delegate(string s)
					{
						OnPresence(l2, s);
					});
					Register(l2, "CDC0112_EventSubmit", delegate(string s)
					{
						OnEventSubmit(l2, s);
					});
					Register(l2, "CDC0112_EventDeliver", delegate(string s)
					{
						OnEventDeliver(l2, s);
					});
				}
				if (!Server && ServerLink(l2) && !l2.Ready && Time.realtimeSinceStartup >= l2.NextHello)
				{
					l2.NextHello = Time.realtimeSinceStartup + 5f;
					Send(l2, "CDC0110_Hello", "0114");
				}
			}
			FlushRoster();
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_UPDATE: " + ex.GetType().Name + " " + ex.Message));
			nextScan += 5f;
		}
	}

	private void OnHello(Link l, string s)
	{
		if (Server)
		{
			if (s != "0114" || !l.Rate.Allow(Time.realtimeSinceStartup))
			{
				return;
			}
			if (!l.Ready)
			{
				try
				{
					EnsureColors();
					object obj = Call(l.Rpc, "GetSocket");
					string text = ColorRules.TransportIdentity(obj.GetType().Name, (string)Call(obj, "GetHostName"));
					l.Color = colorStore.GetOrAssign(text);
					l.Identity = text;
					l.Ready = true;
					rosterDirty = true;
				}
				catch (Exception ex)
				{
					if (!l.Warned)
					{
						l.Warned = true;
						((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_COLOR: " + (ex.InnerException ?? ex).Message));
					}
					Send(l, "CDC0110_Hello", "color-error");
					return;
				}
			}
			Send(l, "CDC0110_Hello", PingRules.Welcome(PeerUid(l), PeerName(l)));
			AnnounceJoin(l);
		}
		else if (ServerLink(l))
		{
			long uid;
			string name;
			if (s == "color-error" && !l.Warned)
			{
				l.Warned = true;
				ShowText("color.error");
			}
			else if (!l.Ready && PingRules.ReadWelcome(s, out uid, out name))
			{
				l.OwnPingUid = uid;
				l.OwnPingName = name;
				l.Ready = true;
				fallbackNotified = false;
				PrepareMessageSound();
				ShowText("welcome", ActiveMode());
			}
		}
	}

	private void OnSubmit(Link l, string s)
	{
		if (!Server || !l.Ready || !l.Rate.Allow(Time.realtimeSinceStartup) || s == null || s.Length > 400)
		{
			return;
		}
		string text = Rules.Clean(s, 400);
		if (text.Length == 0)
		{
			return;
		}
		string id = PeerUid(l).ToString(CultureInfo.InvariantCulture);
		string name = PeerName(l);
		string value = ColorPacket.Pack(id, name, l.Color, text);
		Link[] array = links.Values.ToArray();
		foreach (Link link in array)
		{
			if (link.Ready && Active(link))
			{
				try
				{
					Send(link, "CDC0110_Deliver", value);
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_SEND: " + ex.GetType().Name));
				}
			}
		}
	}

	private void OnDeliver(Link l, string s)
	{
		if (!CanReceive(l) || !ColorPacket.TryRead(s, out var id, out var name, out var color, out var text))
		{
			return;
		}
		names[id] = name;
		if (!muted.Contains(id))
		{
			AddLine("<color=" + color + "><b>" + name + "</b></color>: " + text);
			if (id != l.OwnPingUid.ToString(CultureInfo.InvariantCulture))
			{
				PlayMessageSound();
			}
		}
	}

	private void OnNotice(Link l, string s)
	{
		if (CanReceive(l) && NoticePacket.TryFormat(s, Spanish(), out var text))
		{
			Show(text);
		}
	}

	private void OnControl(Link l, string s)
	{
		if (!Server || !l.Ready || s == null || s.Length > 240 || !l.ControlRate.Allow(Time.realtimeSinceStartup))
		{
			return;
		}
		try
		{
			if (s.Equals("/cplayers", StringComparison.OrdinalIgnoreCase))
			{
				foreach (Link value in links.Values)
				{
					if (value.Ready && Active(value))
					{
						SendNotice(l, "players.row", PeerName(value), value.Identity, value.Color, PeerUid(value).ToString(CultureInfo.InvariantCulture));
					}
				}
				return;
			}
			bool flag = l.Local && l == hostLink;
			if (!flag)
			{
				string text = (string)Call(Call(l.Rpc, "GetSocket"), "GetHostName");
				flag = (bool)Call(currentNet, "IsAdmin", text);
			}
			if (!flag)
			{
				SendNotice(l, "color.admin");
				return;
			}
			string target;
			string text2 = colorStore.AdminCommand(flag, s, out target, (string name) => NameTargets.Resolve(name, from p in links.Values
				where p.Ready && Active(p)
				select new KeyValuePair<string, string>(p.Identity, PeerName(p))));
			foreach (Link value2 in links.Values)
			{
				if (value2.Identity == target)
				{
					value2.Color = text2;
				}
			}
			SendNotice(l, "color.saved", target, text2);
		}
		catch (FormatException ex)
		{
			SendNotice(l, Texts.Has(ex.Message) ? ex.Message : "color.failed");
		}
		catch (InvalidOperationException ex2)
		{
			SendNotice(l, Texts.Has(ex2.Message) ? ex2.Message : "color.failed");
		}
		catch (Exception ex3)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_CHAT_ADMIN: " + (ex3.InnerException ?? ex3).Message));
			SendNotice(l, "color.failed");
		}
	}

	private void AddLine(string s)
	{
		object obj = Instance(chatType);
		if (obj != null)
		{
			hideTimer.SetValue(obj, 0f);
			Call(obj, "AddString", s);
		}
	}

	private void Show(string s)
	{
		AddLine("[Valheim Chat Plus] " + s);
	}

	private static bool Input(object __instance)
	{
		if ((Object)(object)self == (Object)null)
		{
			return true;
		}
		object obj = Get(__instance, "m_input");
		string text = (string)AccessTools.Property(obj.GetType(), "text").GetValue(obj);
		InputRoute inputRoute = Rules.Route(text, self.defaultGlobal.Value);
		if (inputRoute == InputRoute.Vanilla)
		{
			return true;
		}
		try
		{
			if (inputRoute == InputRoute.Global)
			{
				if (Rules.ResolveAvailability(inputRoute, self.GlobalAvailable()) == InputRoute.Vanilla)
				{
					if (!self.fallbackNotified)
					{
						self.fallbackNotified = true;
						self.ShowText("fallback");
					}
					return true;
				}
				self.fallbackNotified = false;
				self.SendGlobal(text);
			}
			else
			{
				self.Command(text.Trim());
			}
		}
		catch (Exception ex)
		{
			self.ShowText("action.error");
			((BaseUnityPlugin)self).Logger.LogWarning((object)ex);
		}
		return false;
	}

	private void Command(string text)
	{
		completion.Reset();
		if (text.Equals("/chathelp", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/chathelp ", StringComparison.OrdinalIgnoreCase))
		{
			ShowHelp();
		}
		else if (WhisperRules.IsCommand(text))
		{
			SendWhisper(text);
		}
		else if (ReplyState.IsCommand(text))
		{
			SendReply(text);
		}
		else if (text.Equals("/chatmode", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/chatmode ", StringComparison.OrdinalIgnoreCase))
		{
			string text2 = text.Substring(9).Trim().ToLowerInvariant();
			if (text2 == "status")
			{
				ShowText("mode.status", PreferredMode(), ActiveMode());
				return;
			}
			if (text2 != "" && text2 != "global" && text2 != "vanilla")
			{
				ShowText("mode.usage");
				return;
			}
			bool value = defaultGlobal.Value;
			try
			{
				defaultGlobal.Value = ((text2 == "") ? (!value) : (text2 == "global"));
				((BaseUnityPlugin)this).Config.Save();
			}
			catch
			{
				try
				{
					defaultGlobal.Value = value;
				}
				catch
				{
				}
				ShowText("mode.save_error");
				return;
			}
			ShowText("mode.saved", PreferredMode(), ActiveMode());
		}
		else if (text.Equals("/cplayers", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/cplayers ", StringComparison.OrdinalIgnoreCase) || text.Equals("/chatcolor", StringComparison.OrdinalIgnoreCase) || text.StartsWith("/chatcolor ", StringComparison.OrdinalIgnoreCase))
		{
			Link link = MessagingServer();
			if (link == null)
			{
				ShowText("offline");
			}
			else
			{
				Send(link, "CDC0110_Control", text);
			}
		}
		else if (text.StartsWith("/cmute", StringComparison.OrdinalIgnoreCase))
		{
			string text3 = text.Substring(6).Trim();
			if (!long.TryParse(text3, out var _))
			{
				ShowText("mute.usage");
			}
			else if (muted.Remove(text3))
			{
				ShowText("mute.off", text3);
			}
			else
			{
				muted.Add(text3);
				ShowText("mute.on", text3);
			}
		}
		else
		{
			SendGlobal((text.Length > 2) ? text.Substring(2) : "");
		}
	}

	private void SendGlobal(string text)
	{
		string text2 = Rules.Clean(text, 400);
		if (text2.Length == 0)
		{
			ShowText("global.usage");
			return;
		}
		Link link = MessagingServer();
		if (link == null)
		{
			ShowText("offline");
		}
		else
		{
			Send(link, "CDC0110_Submit", text2);
		}
	}

	private void OnDestroy()
	{
		ReleaseMessageSound();
		Harmony obj = eventHarmony;
		if (obj != null)
		{
			obj.UnpatchSelf();
		}
		Harmony obj2 = tabHarmony;
		if (obj2 != null)
		{
			obj2.UnpatchSelf();
		}
		Harmony obj3 = pingHarmony;
		if (obj3 != null)
		{
			obj3.UnpatchSelf();
		}
		Harmony obj4 = harmony;
		if (obj4 != null)
		{
			obj4.UnpatchSelf();
		}
		if ((Object)(object)self == (Object)(object)this)
		{
			self = null;
		}
	}

	private void SetupHosting()
	{
		hostUid = AccessTools.Method(netType, "GetUID", Type.EmptyTypes, (Type[])null);
		hostIsDedicated = AccessTools.Method(netType, "IsDedicated", Type.EmptyTypes, (Type[])null);
		if (playerType != null)
		{
			hostPlayerId = AccessTools.Method(playerType, "GetPlayerID", Type.EmptyTypes, (Type[])null);
			hostPlayerName = AccessTools.Method(playerType, "GetPlayerName", Type.EmptyTypes, (Type[])null);
		}
	}

	private void ResetHost()
	{
		hostLink = null;
		hostPlayer = null;
		hostWarning = false;
		nextHostAttempt = 0f;
	}

	private void EnsureColors()
	{
		if (colorStoreFailed)
		{
			throw new InvalidOperationException("Color file unavailable; repair it and restart the server.");
		}
		if (colorStore != null)
		{
			return;
		}
		try
		{
			colorStore = ColorStore.Load(Path.Combine(Paths.ConfigPath, "CrossplayDirectChat", "colors-v1.tsv"));
		}
		catch
		{
			colorStoreFailed = true;
			throw;
		}
	}

	private void EnsureHost()
	{
		if (!Server || Time.realtimeSinceStartup < nextHostAttempt)
		{
			return;
		}
		object obj = localPlayer?.GetValue(null);
		if (obj == null || (hostLink != null && obj == hostPlayer))
		{
			return;
		}
		try
		{
			if (hostUid == null || hostPlayerId == null || hostPlayerName == null || hostIsDedicated == null)
			{
				throw new MissingMemberException("Local host identity methods unavailable.");
			}
			if ((bool)hostIsDedicated.Invoke(currentNet, null))
			{
				return;
			}
			long num = Convert.ToInt64(hostUid.Invoke(currentNet, null), CultureInfo.InvariantCulture);
			long num2 = Convert.ToInt64(hostPlayerId.Invoke(obj, null), CultureInfo.InvariantCulture);
			string text = Rules.Clean(hostPlayerName.Invoke(obj, null) as string, 64);
			if (num == 0L || num2 == 0L || text.Length == 0)
			{
				return;
			}
			string text2 = "Host_" + num2.ToString(CultureInfo.InvariantCulture);
			if (hostLink != null && hostLink.Identity == text2 && hostLink.OwnPingUid == num)
			{
				hostPlayer = obj;
				hostLink.OwnPingName = text;
				hostLink.PresenceName = text;
				hostLink.Sleeping = false;
				localInBedState = false;
				rosterDirty = true;
				return;
			}
			EnsureColors();
			string orAssign = colorStore.GetOrAssign(text2);
			if (hostLink != null)
			{
				links.Remove(hostLink.Rpc);
				AnnounceLeave(hostLink);
			}
			hostLink = new Link
			{
				Local = true,
				Ready = true,
				Rpc = new object(),
				Identity = text2,
				Color = orAssign,
				OwnPingUid = num,
				OwnPingName = text
			};
			hostPlayer = obj;
			links.Add(hostLink.Rpc, hostLink);
			localInBedState = false;
			fallbackNotified = false;
			rosterDirty = true;
			PrepareMessageSound();
			ShowText("welcome", ActiveMode());
			AnnounceJoin(hostLink);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"VALHEIM_CHAT_PLUS_HOST_READY 0.1.16");
		}
		catch (Exception ex)
		{
			nextHostAttempt = Time.realtimeSinceStartup + 5f;
			if (!hostWarning)
			{
				hostWarning = true;
				((BaseUnityPlugin)this).Logger.LogWarning((object)("VALHEIM_CHAT_PLUS_HOST_UNAVAILABLE: " + (ex.InnerException ?? ex).Message));
				ShowText("host.error");
			}
		}
	}

	private long PeerUid(Link l)
	{
		if (!l.Local)
		{
			return Convert.ToInt64(Get(l.Peer, "m_uid"), CultureInfo.InvariantCulture);
		}
		return l.OwnPingUid;
	}

	private Link MessagingServer()
	{
		if (currentNet == null)
		{
			return null;
		}
		if (Server)
		{
			EnsureHost();
			if (hostLink == null || !hostLink.Ready || !Active(hostLink))
			{
				return null;
			}
			return hostLink;
		}
		foreach (Link value in links.Values)
		{
			if (value.Ready && Active(value) && ServerLink(value))
			{
				return value;
			}
		}
		return null;
	}

	private bool CanReceive(Link l)
	{
		if (l != null && l.Ready && Active(l))
		{
			if (!l.Local)
			{
				if (!Server)
				{
					return ServerLink(l);
				}
				return false;
			}
			if (Server)
			{
				return l == hostLink;
			}
			return false;
		}
		return false;
	}

	private void DispatchHost(Link l, string channel, string packet)
	{
		if (l != hostLink || !l.Ready || !Active(l))
		{
			throw new InvalidOperationException("Inactive local host endpoint.");
		}
		switch (channel)
		{
		case "CDC0110_Submit":
			OnSubmit(l, packet);
			break;
		case "CDC0110_Control":
			OnControl(l, packet);
			break;
		case "CDC0110_WhisperSubmit":
			OnWhisperSubmit(l, packet);
			break;
		case "CDC0110_PingSubmit":
			OnPingSubmit(l, packet);
			break;
		case "CDC0112_EventSubmit":
			OnEventSubmit(l, packet);
			break;
		case "CDC0110_Deliver":
			OnDeliver(l, packet);
			break;
		case "CDC0110_Notice":
			OnNotice(l, packet);
			break;
		case "CDC0110_WhisperDeliver":
			OnWhisperDeliver(l, packet);
			break;
		case "CDC0110_WhisperNotice":
			OnWhisperNotice(l, packet);
			break;
		case "CDC0110_WhisperRoster":
			OnWhisperRoster(l, packet);
			break;
		case "CDC0110_Presence":
			OnPresence(l, packet);
			break;
		case "CDC0110_PingDeliver":
			OnPingDeliver(l, packet);
			break;
		case "CDC0112_EventDeliver":
			OnEventDeliver(l, packet);
			break;
		default:
			throw new InvalidOperationException("Unsupported local host channel.");
		}
	}

	private void SetupLocalization()
	{
		localizationType = AccessTools.TypeByName("Localization");
		if (localizationType != null)
		{
			selectedLanguage = AccessTools.Method(localizationType, "GetSelectedLanguage", Type.EmptyTypes, (Type[])null);
		}
	}

	private bool Spanish()
	{
		try
		{
			if (selectedLanguage == null)
			{
				return false;
			}
			object obj = Instance(localizationType);
			return obj != null && Texts.IsSpanish(selectedLanguage.Invoke(obj, null) as string);
		}
		catch
		{
			return false;
		}
	}

	private string T(string key, params object[] args)
	{
		return Texts.Get(key, Spanish(), args);
	}

	private void ShowText(string key, params object[] args)
	{
		Show(T(key, args));
	}

	private bool GlobalAvailable()
	{
		return MessagingServer() != null;
	}

	private string PreferredMode()
	{
		return T(defaultGlobal.Value ? "mode.global" : "mode.vanilla");
	}

	private string ActiveMode()
	{
		return T((defaultGlobal.Value && GlobalAvailable()) ? "mode.global" : "mode.vanilla");
	}

	private void ShowHelp()
	{
		for (int i = 1; i <= 7; i++)
		{
			ShowText("help." + i);
		}
	}

	private void SendNotice(Link l, string key, params string[] args)
	{
		Send(l, "CDC0110_Notice", NoticePacket.Pack(key, args));
	}

	private void SetupPings()
	{
		//IL_0229: Unknown result type (might be due to invalid IL or missing references)
		//IL_0233: Expected O, but got Unknown
		//IL_024a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0258: Expected O, but got Unknown
		//IL_0270: Unknown result type (might be due to invalid IL or missing references)
		//IL_027e: Expected O, but got Unknown
		try
		{
			Type type = AccessTools.TypeByName("Player");
			pingUserType = AccessTools.TypeByName("UserInfo");
			pingMapType = AccessTools.TypeByName("Minimap");
			pingLocalPlayer = AccessTools.Field(type, "m_localPlayer");
			pingUserName = AccessTools.Field(pingUserType, "Name");
			pingMapMode = AccessTools.Field(pingMapType, "m_mode");
			pingNoMapDistance = AccessTools.Field(pingMapType, "m_nomapPingDistance");
			pingInIntro = AccessTools.Method(type, "InIntro", Type.EmptyTypes, (Type[])null);
			pingAddWorldText = AccessTools.Method(chatType, "AddInworldText", (Type[])null, (Type[])null);
			MethodInfo methodInfo = AccessTools.Method(chatType, "SendPing", new Type[1] { typeof(Vector3) }, (Type[])null);
			MethodInfo methodInfo2 = AccessTools.Method(pingUserType, "GetDisplayName", Type.EmptyTypes, (Type[])null);
			if (pingLocalPlayer == null || pingUserName == null || pingMapMode == null || pingNoMapDistance == null || pingInIntro == null || pingAddWorldText == null || methodInfo == null || methodInfo2 == null)
			{
				throw new MissingMemberException("Required native ping members unavailable.");
			}
			ParameterInfo[] parameters = pingAddWorldText.GetParameters();
			if (parameters.Length != 6 || parameters[0].ParameterType != typeof(GameObject) || parameters[1].ParameterType != typeof(long) || parameters[2].ParameterType != typeof(Vector3) || parameters[4].ParameterType != pingUserType || parameters[5].ParameterType != typeof(string))
			{
				throw new MissingMethodException("Unsupported AddInworldText signature.");
			}
			pingType = Enum.Parse(parameters[3].ParameterType, "Ping");
			pingNoMap = Enum.Parse(pingMapMode.FieldType, "None");
			pingHarmony = new Harmony("local.crossplaydirectchat.pings");
			pingHarmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "SendDirectPing", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			pingHarmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(Plugin), "DirectPingName", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			pingsReady = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"DIRECT_PINGS_READY 0.1.16");
		}
		catch (Exception ex)
		{
			pingsReady = false;
			Harmony obj = pingHarmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PINGS_DISABLED: " + ex.Message + "; text chat remains available."));
		}
	}

	private static bool DirectPingName(object __instance, ref string __result)
	{
		if ((Object)(object)self != (Object)null && self.pingNames.TryGetValue(__instance, out var value))
		{
			__result = value.Name;
			return false;
		}
		return true;
	}

	private static bool SendDirectPing(Vector3 __0)
	{
		//IL_0066: Unknown result type (might be due to invalid IL or missing references)
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_007b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0081: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)self == (Object)null || !self.pingsReady || self.currentNet == null)
		{
			return true;
		}
		Link link = self.MessagingServer();
		if (link == null)
		{
			return true;
		}
		try
		{
			object? value = self.pingLocalPlayer.GetValue(null);
			Component val = (Component)((value is Component) ? value : null);
			if ((Object)(object)val == (Object)null)
			{
				return false;
			}
			__0.y = val.transform.position.y;
			string text = PingRules.Pack(__0.x, __0.y, __0.z);
			if (!PingRules.Position(text, out var _, out var _, out var _) || !link.OutgoingPings.Allow(Time.realtimeSinceStartup))
			{
				return false;
			}
			self.RenderDirectPing(link.OwnPingUid, link.OwnPingName, __0.x, __0.y, __0.z);
			self.Send(link, "CDC0110_PingSubmit", text);
		}
		catch (Exception e)
		{
			self.PingError(e);
		}
		return false;
	}

	private void OnPingSubmit(Link l, string s)
	{
		if (!Server || !l.Ready || !l.Pings.Allow(Time.realtimeSinceStartup) || !PingRules.Position(s, out var x, out var y, out var z))
		{
			return;
		}
		long uid = PeerUid(l);
		string name = PeerName(l);
		string value = PingRules.Delivery(uid, name, x, y, z);
		foreach (Link value2 in links.Values)
		{
			if (value2 != l && value2.Ready && Active(value2))
			{
				try
				{
					Send(value2, "CDC0110_PingDeliver", value);
				}
				catch (Exception e)
				{
					PingError(e);
				}
			}
		}
	}

	private void OnPingDeliver(Link l, string s)
	{
		if (CanReceive(l) && pingsReady && PingRules.Read(s, out var uid, out var name, out var x, out var y, out var z))
		{
			RenderDirectPing(uid, name, x, y, z);
		}
	}

	private void RenderDirectPing(long uid, string name, float x, float y, float z)
	{
		//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
		//IL_006e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0073: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			object? value = pingLocalPlayer.GetValue(null);
			Component val = (Component)((value is Component) ? value : null);
			if ((Object)(object)val == (Object)null || (bool)pingInIntro.Invoke(val, null))
			{
				return;
			}
			Vector3 val2 = default(Vector3);
			((Vector3)(ref val2))..ctor(x, y, z);
			object obj = Instance(pingMapType);
			if (obj == null || !pingNoMap.Equals(pingMapMode.GetValue(obj)) || !(Vector3.Distance(val.transform.position, val2) > (float)pingNoMapDistance.GetValue(obj)))
			{
				object obj2 = Instance(chatType);
				if (obj2 != null)
				{
					object obj3 = Activator.CreateInstance(pingUserType);
					pingUserName.SetValue(obj3, name);
					pingNames.Add(obj3, new PingLabel
					{
						Name = name
					});
					pingAddWorldText.Invoke(obj2, new object[6] { null, uid, val2, pingType, obj3, "" });
				}
			}
		}
		catch (Exception e)
		{
			PingError(e);
		}
	}

	private void PingError(Exception e)
	{
		if (!(Time.realtimeSinceStartup < nextPingError))
		{
			nextPingError = Time.realtimeSinceStartup + 5f;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PING: " + (e.InnerException ?? e).Message));
		}
	}

	private void SetupWhispers()
	{
		//IL_0268: Unknown result type (might be due to invalid IL or missing references)
		//IL_0272: Expected O, but got Unknown
		//IL_0289: Unknown result type (might be due to invalid IL or missing references)
		//IL_0297: Expected O, but got Unknown
		whisperTab = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "WhisperTabCompletion", true, "Complete /w player names only when Tab is pressed. Disable to compare performance; whispers remain available.");
		try
		{
			Type type = AccessTools.TypeByName("Terminal");
			MethodInfo methodInfo = AccessTools.Method(type, "UpdateInput", Type.EmptyTypes, (Type[])null);
			FieldInfo fieldInfo = AccessTools.Field(type, "m_input");
			Type type2 = AccessTools.TypeByName("ZInput");
			Type type3 = AccessTools.TypeByName("UnityEngine.KeyCode");
			MethodInfo methodInfo2 = AccessTools.Method(type2, "GetKeyDown", new Type[2]
			{
				type3,
				typeof(bool)
			}, (Type[])null);
			if (methodInfo == null || fieldInfo == null || methodInfo2 == null)
			{
				throw new MissingMemberException("Native input members unavailable.");
			}
			ParameterExpression parameterExpression = Expression.Parameter(typeof(object), "terminal");
			MemberExpression memberExpression = Expression.Field(Expression.Convert(parameterExpression, type), fieldInfo);
			inputFocused = InputGetter<bool>(parameterExpression, memberExpression, "isFocused");
			inputText = InputGetter<string>(parameterExpression, memberExpression, "text");
			inputCaret = InputGetter<int>(parameterExpression, memberExpression, "caretPosition");
			inputAnchor = InputGetter<int>(parameterExpression, memberExpression, "selectionAnchorPosition");
			inputFocus = InputGetter<int>(parameterExpression, memberExpression, "selectionFocusPosition");
			MethodInfo method = AccessTools.Method(fieldInfo.FieldType, "IsCompositionActive", Type.EmptyTypes, (Type[])null);
			inputComposing = Expression.Lambda<Func<object, bool>>(Expression.Call(memberExpression, method), new ParameterExpression[1] { parameterExpression }).Compile();
			ParameterExpression parameterExpression2 = Expression.Parameter(typeof(string), "value");
			replaceInput = Expression.Lambda<Action<object, string>>(Expression.Call(memberExpression, AccessTools.Property(fieldInfo.FieldType, "text").SetMethod, parameterExpression2), new ParameterExpression[2] { parameterExpression, parameterExpression2 }).Compile();
			ParameterExpression parameterExpression3 = Expression.Parameter(typeof(int), "caret");
			replaceCaret = Expression.Lambda<Action<object, int>>(Expression.Call(memberExpression, AccessTools.Property(fieldInfo.FieldType, "caretPosition").SetMethod, parameterExpression3), new ParameterExpression[2] { parameterExpression, parameterExpression3 }).Compile();
			tabPressed = Expression.Lambda<Func<bool>>(Expression.Call(methodInfo2, Expression.Constant(Enum.Parse(type3, "Tab"), type3), Expression.Constant(true)), Array.Empty<ParameterExpression>()).Compile();
			tabHarmony = new Harmony("local.crossplaydirectchat.whisper-tab");
			tabHarmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "CompleteWhisperTab", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			tabReady = true;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"DIRECT_WHISPER_TAB_READY 0.1.16");
		}
		catch (Exception ex)
		{
			tabReady = false;
			Harmony obj = tabHarmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_WHISPER_TAB_DISABLED: " + ex.GetType().Name + "; /w remains available."));
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)"DIRECT_WHISPERS_READY 0.1.16");
	}

	private static Func<object, T> InputGetter<T>(ParameterExpression obj, Expression field, string name)
	{
		return Expression.Lambda<Func<object, T>>(Expression.Property(field, AccessTools.Property(field.Type, name)), new ParameterExpression[1] { obj }).Compile();
	}

	private static bool CompleteWhisperTab(object __instance)
	{
		Plugin plugin = self;
		if ((Object)(object)plugin == (Object)null || !plugin.tabReady || !plugin.whisperTab.Value || !plugin.chatType.IsInstanceOfType(__instance))
		{
			return true;
		}
		bool flag = false;
		try
		{
			if (!plugin.tabPressed() || !plugin.inputFocused(__instance))
			{
				return true;
			}
			string text = plugin.inputText(__instance);
			if (!WhisperRules.IsCommand(text))
			{
				plugin.completion.Reset();
				return true;
			}
			flag = true;
			if (plugin.inputComposing(__instance) || plugin.inputAnchor(__instance) != plugin.inputFocus(__instance))
			{
				return false;
			}
			if (plugin.completion.Complete(text, plugin.inputCaret(__instance), plugin.roster, out var result, out var position))
			{
				plugin.replaceInput(__instance, result);
				plugin.replaceCaret(__instance, position);
			}
			return false;
		}
		catch (Exception ex)
		{
			plugin.tabReady = false;
			plugin.completion.Reset();
			((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_WHISPER_TAB_DISABLED: " + ex.GetType().Name));
			return !flag;
		}
	}

	private void ResetWhispers()
	{
		roster = Array.Empty<WhisperPlayer>();
		rosterDirty = false;
		completion.Reset();
		reply.Reset();
		soundGate.Reset();
	}

	private void RosterChanged()
	{
		if (Server)
		{
			rosterDirty = true;
			return;
		}
		roster = Array.Empty<WhisperPlayer>();
		completion.Reset();
		reply.Reset();
		soundGate.Reset();
	}

	private WhisperPlayer PlayerFor(Link l)
	{
		return new WhisperPlayer(l.Identity, PeerUid(l), PeerName(l));
	}

	private void FlushRoster()
	{
		if (!rosterDirty)
		{
			return;
		}
		rosterDirty = false;
		if (!Server)
		{
			return;
		}
		List<WhisperPlayer> list = new List<WhisperPlayer>();
		foreach (Link value2 in links.Values)
		{
			if (value2.Ready && Active(value2))
			{
				list.Add(PlayerFor(value2));
			}
		}
		string value = "";
		try
		{
			string text = WhisperRules.Roster(list);
			if (WhisperRules.ReadRoster(text, out var _))
			{
				value = text;
			}
		}
		catch (InvalidOperationException)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)"DIRECT_WHISPER_ROSTER_UNAVAILABLE");
		}
		foreach (Link value3 in links.Values)
		{
			if (value3.Ready && Active(value3))
			{
				try
				{
					Send(value3, "CDC0110_WhisperRoster", value);
				}
				catch (Exception ex2)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_WHISPER_ROSTER: " + ex2.GetType().Name));
				}
			}
		}
	}

	private void OnWhisperRoster(Link l, string packet)
	{
		if (CanReceive(l))
		{
			completion.Reset();
			roster = (WhisperRules.ReadRoster(packet, out var players) ? players : Array.Empty<WhisperPlayer>());
		}
	}

	private Link WhisperServer()
	{
		return MessagingServer();
	}

	private void SendWhisper(string text)
	{
		if (!WhisperRules.Parse(text, out var target, out var message))
		{
			WhisperError("usage");
			return;
		}
		Link link = WhisperServer();
		if (link == null)
		{
			WhisperError("offline");
			return;
		}
		WhisperPlayer whisperPlayer = WhisperRules.Resolve(target, roster);
		if (whisperPlayer == null)
		{
			WhisperError("target");
		}
		else
		{
			SendPrivate(link, whisperPlayer, message);
		}
	}

	private void SendReply(string text)
	{
		if (!ReplyState.Parse(text, out var message))
		{
			WhisperError("replyusage");
			return;
		}
		Link link = WhisperServer();
		if (link == null)
		{
			WhisperError("offline");
			return;
		}
		if (!reply.HasSender)
		{
			WhisperError("noreply");
			return;
		}
		WhisperPlayer whisperPlayer = reply.Resolve(roster);
		if (whisperPlayer == null)
		{
			WhisperError("target");
		}
		else
		{
			SendPrivate(link, whisperPlayer, message);
		}
	}

	private void SendPrivate(Link server, WhisperPlayer player, string message)
	{
		if (muted.Contains(player.Uid.ToString(CultureInfo.InvariantCulture)))
		{
			WhisperError("muted");
		}
		else
		{
			Send(server, "CDC0110_WhisperSubmit", WhisperRules.Submit(player.Id, message));
		}
	}

	private void OnWhisperSubmit(Link l, string packet)
	{
		if (!Server || !l.Ready || !l.Rate.Allow(Time.realtimeSinceStartup))
		{
			return;
		}
		if (!WhisperRules.ReadSubmit(packet, out var id, out var message))
		{
			Send(l, "CDC0110_WhisperNotice", "usage");
			return;
		}
		Link link = null;
		foreach (Link value in links.Values)
		{
			if (value.Ready && value.Identity == id && Active(value))
			{
				if (link != null)
				{
					Send(l, "CDC0110_WhisperNotice", "target");
					return;
				}
				link = value;
			}
		}
		if (link == null)
		{
			Send(l, "CDC0110_WhisperNotice", "target");
			return;
		}
		string packet2 = WhisperRules.Delivery(PlayerFor(l), PlayerFor(link), message);
		try
		{
			WhisperRules.Deliver(l, link, packet2, delegate(Link peer, string value)
			{
				Send(peer, "CDC0110_WhisperDeliver", value);
			});
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_WHISPER_SEND: " + ex.GetType().Name));
			Send(l, "CDC0110_WhisperNotice", "send");
		}
	}

	private void OnWhisperDeliver(Link l, string packet)
	{
		if (CanReceive(l) && WhisperRules.ReadDelivery(packet, l.OwnPingUid, out var from, out var fromName, out var _, out var toName, out var message, out var senderId) && (from == l.OwnPingUid || !muted.Contains(from.ToString(CultureInfo.InvariantCulture))))
		{
			AddLine(ChatPresentation.Whisper(from == l.OwnPingUid, fromName, toName, message, Spanish()));
			if (from != l.OwnPingUid)
			{
				reply.Remember(senderId);
				PlayMessageSound();
			}
		}
	}

	private void OnWhisperNotice(Link l, string code)
	{
		if (CanReceive(l) && code != null && code.Length <= 16)
		{
			WhisperError(code);
		}
	}

	private void WhisperError(string code)
	{
		if (Texts.Has("whisper." + code))
		{
			ShowText("whisper." + code);
		}
	}

	private void AnnounceJoin(Link l)
	{
		if (Server && l.Ready && l.PresenceName == null)
		{
			l.PresenceName = PeerName(l);
			if (l.PresenceName.Length > 0)
			{
				BroadcastPresence(joined: true, l.PresenceName);
			}
		}
	}

	private void AnnounceLeave(Link l)
	{
		if (Server && l.PresenceName != null)
		{
			string presenceName = l.PresenceName;
			l.PresenceName = null;
			if (presenceName.Length > 0)
			{
				BroadcastPresence(joined: false, presenceName);
			}
		}
	}

	private void BroadcastPresence(bool joined, string name)
	{
		string value = ChatPresentation.PresencePacket(joined, name);
		foreach (Link value2 in links.Values)
		{
			try
			{
				if (value2.Ready && Active(value2))
				{
					Send(value2, "CDC0110_Presence", value);
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PRESENCE_SEND: " + ex.GetType().Name));
			}
		}
	}

	private void OnPresence(Link l, string packet)
	{
		if (CanReceive(l) && ChatPresentation.ReadPresence(packet, out var joined, out var name))
		{
			AddLine(ChatPresentation.PresenceLine(joined, name, Spanish()));
		}
	}

	private void SetupPlayerEvents()
	{
		//IL_0006: Unknown result type (might be due to invalid IL or missing references)
		//IL_0010: Expected O, but got Unknown
		//IL_033e: Unknown result type (might be due to invalid IL or missing references)
		//IL_034b: Expected O, but got Unknown
		//IL_03ed: Unknown result type (might be due to invalid IL or missing references)
		//IL_03fb: Expected O, but got Unknown
		//IL_029a: Unknown result type (might be due to invalid IL or missing references)
		//IL_02a7: Expected O, but got Unknown
		eventHarmony = new Harmony("local.crossplaydirectchat.player-events");
		playerType = AccessTools.TypeByName("Player");
		characterType = AccessTools.TypeByName("Character");
		zdoType = AccessTools.TypeByName("ZDO");
		zdoVarsType = AccessTools.TypeByName("ZDOVars");
		znetViewType = AccessTools.TypeByName("ZNetView");
		localPlayer = ((playerType == null) ? null : AccessTools.Field(playerType, "m_localPlayer"));
		lastHit = ((characterType == null) ? null : AccessTools.Field(characterType, "m_lastHit"));
		characterName = ((characterType == null) ? null : AccessTools.Field(characterType, "m_name"));
		netView = ((characterType == null) ? null : AccessTools.Field(characterType, "m_nview"));
		zdoUid = ((zdoType == null) ? null : AccessTools.Field(zdoType, "m_uid"));
		getZdo = ((znetViewType == null) ? null : AccessTools.Method(znetViewType, "GetZDO", Type.EmptyTypes, (Type[])null));
		inBedField = ((zdoVarsType == null) ? null : AccessTools.Field(zdoVarsType, "s_inBed"));
		if (localizationType != null)
		{
			localize = AccessTools.Method(localizationType, "Localize", new Type[1] { typeof(string) }, (Type[])null);
		}
		if (playerType == null || localPlayer == null)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)"DIRECT_PLAYER_EVENTS_DISABLED: Player.m_localPlayer unavailable.");
			return;
		}
		try
		{
			if (zdoType == null || inBedField == null || netView == null || getZdo == null)
			{
				throw new MissingMemberException("ZDO/ZDOVars.s_inBed/Character.m_nview");
			}
			inBedHash = Convert.ToInt32(inBedField.GetValue(null), CultureInfo.InvariantCulture);
			MethodInfo methodInfo = AccessTools.Method(zdoType, "Set", new Type[2]
			{
				typeof(int),
				typeof(bool)
			}, (Type[])null);
			if (methodInfo == null)
			{
				throw new MissingMethodException("ZDO.Set(int,bool)");
			}
			eventHarmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "PlayerInBedFlagChanged", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			sleepIntentPatchReady = true;
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_SLEEP_INTENT_DISABLED: " + ex.GetType().Name + " " + ex.Message));
		}
		try
		{
			MethodInfo methodInfo2 = playerType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo m) => m.Name == "SetSleeping" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(bool));
			if (methodInfo2 == null)
			{
				throw new MissingMethodException("Player.SetSleeping(bool)");
			}
			eventHarmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "PlayerSleepingChanged", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			sleepStatePatchReady = true;
		}
		catch (Exception ex2)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_SLEEP_FALLBACK_DISABLED: " + ex2.GetType().Name + " " + ex2.Message));
		}
		sleepPatchReady = sleepIntentPatchReady || sleepStatePatchReady;
		try
		{
			MethodInfo methodInfo3 = AccessTools.Method(playerType, "OnDeath", Type.EmptyTypes, (Type[])null);
			if (methodInfo3 == null || lastHit == null)
			{
				throw new MissingMemberException("Player.OnDeath/Character.m_lastHit");
			}
			eventHarmony.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeof(Plugin), "PlayerDied", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			deathPatchReady = true;
		}
		catch (Exception ex3)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_DEATH_EVENT_DISABLED: " + ex3.GetType().Name + " " + ex3.Message));
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)("DIRECT_PLAYER_EVENTS_READY 0.1.16 sleep=" + sleepPatchReady + " sleepIntent=" + sleepIntentPatchReady + " sleepFallback=" + sleepStatePatchReady + " death=" + deathPatchReady));
	}

	private static void PlayerInBedFlagChanged(object __instance, int __0, bool __1)
	{
		Plugin plugin = self;
		if ((Object)(object)plugin == (Object)null || !plugin.sleepIntentPatchReady || __0 != plugin.inBedHash || !plugin.IsLocalPlayerZdo(__instance))
		{
			return;
		}
		try
		{
			plugin.ReportSleeping(__1);
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_SLEEP_INTENT: " + ex.GetType().Name));
		}
	}

	private static void PlayerSleepingChanged(object __instance, bool __0)
	{
		Plugin plugin = self;
		if ((Object)(object)plugin == (Object)null || !plugin.sleepStatePatchReady || !plugin.IsLocalPlayer(__instance))
		{
			return;
		}
		try
		{
			plugin.ReportSleeping(__0);
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_SLEEP_EVENT: " + ex.GetType().Name));
		}
	}

	private void ReportSleeping(bool sleeping)
	{
		if (localInBedState != sleeping && SendPlayerEvent(PlayerEventRules.SleepSubmit(sleeping)))
		{
			localInBedState = sleeping;
		}
	}

	private static void PlayerDied(object __instance)
	{
		Plugin plugin = self;
		if ((Object)(object)plugin == (Object)null || !plugin.deathPatchReady || !plugin.IsLocalPlayer(__instance))
		{
			return;
		}
		try
		{
			plugin.ReadDeath(__instance, out var hitType, out var attackerName);
			plugin.SendPlayerEvent(PlayerEventRules.DeathSubmit(hitType, attackerName));
		}
		catch (Exception ex)
		{
			((BaseUnityPlugin)plugin).Logger.LogWarning((object)("DIRECT_DEATH_EVENT: " + ex.GetType().Name));
		}
	}

	private bool IsLocalPlayer(object player)
	{
		if (player != null)
		{
			return localPlayer?.GetValue(null) == player;
		}
		return false;
	}

	private bool IsLocalPlayerZdo(object zdo)
	{
		if (zdo == null)
		{
			return false;
		}
		try
		{
			object obj = localPlayer?.GetValue(null);
			if (obj == null)
			{
				return false;
			}
			object obj2 = netView?.GetValue(obj);
			if (obj2 == null)
			{
				return false;
			}
			object obj3 = getZdo?.Invoke(obj2, null);
			if (obj3 == null)
			{
				return false;
			}
			if (obj3 == zdo)
			{
				return true;
			}
			if (zdoUid == null)
			{
				return false;
			}
			object value = zdoUid.GetValue(obj3);
			object value2 = zdoUid.GetValue(zdo);
			return value?.Equals(value2) ?? false;
		}
		catch
		{
			return false;
		}
	}

	private bool SendPlayerEvent(string packet)
	{
		Link link = MessagingServer();
		if (link == null)
		{
			return false;
		}
		Send(link, "CDC0112_EventSubmit", packet);
		return true;
	}

	private void ReadDeath(object player, out int hitType, out string attackerName)
	{
		hitType = 0;
		attackerName = "";
		object obj = lastHit?.GetValue(player);
		if (obj == null)
		{
			return;
		}
		FieldInfo fieldInfo = AccessTools.Field(obj.GetType(), "m_hitType");
		if (fieldInfo != null)
		{
			try
			{
				int num = Convert.ToInt32(fieldInfo.GetValue(obj), CultureInfo.InvariantCulture);
				if (num >= 0 && num <= 24)
				{
					hitType = num;
				}
			}
			catch
			{
			}
		}
		try
		{
			object obj3 = AccessTools.Method(obj.GetType(), "GetAttacker", Type.EmptyTypes, (Type[])null)?.Invoke(obj, null);
			if (obj3 == null)
			{
				return;
			}
			string text = "";
			MethodInfo methodInfo = AccessTools.Method(obj3.GetType(), "GetPlayerName", Type.EmptyTypes, (Type[])null);
			if (methodInfo != null)
			{
				text = methodInfo.Invoke(obj3, null) as string;
			}
			bool token = false;
			if (string.IsNullOrWhiteSpace(text))
			{
				text = characterName?.GetValue(obj3) as string;
				if (string.IsNullOrWhiteSpace(text))
				{
					MethodInfo methodInfo2 = AccessTools.Method(obj3.GetType(), "GetHoverName", Type.EmptyTypes, (Type[])null);
					if (methodInfo2 != null)
					{
						text = methodInfo2.Invoke(obj3, null) as string;
					}
				}
				token = text?.StartsWith("$", StringComparison.Ordinal) ?? false;
			}
			attackerName = (string.IsNullOrWhiteSpace(text) ? "" : DeathName.Pack(text, token));
		}
		catch
		{
			attackerName = "";
		}
	}

	private string LocalizeName(string value)
	{
		string text = Rules.Clean(value, 80);
		if (text.Length == 0)
		{
			return "";
		}
		try
		{
			if (localize != null && text.IndexOf('$') >= 0)
			{
				string text2 = localize.Invoke(Instance(localizationType), new object[1] { text }) as string;
				if (!string.IsNullOrWhiteSpace(text2))
				{
					text = Rules.Clean(text2, 80);
				}
			}
		}
		catch
		{
		}
		return text;
	}

	private void OnEventSubmit(Link l, string packet)
	{
		if (!Server || !l.Ready || !l.EventRate.Allow(Time.realtimeSinceStartup) || !PlayerEventRules.ReadSubmit(packet, out var kind, out var sleeping, out var hitType, out var attacker))
		{
			return;
		}
		switch (kind)
		{
		case PlayerEventKind.Sleep:
			if (l.Sleeping != sleeping)
			{
				l.Sleeping = sleeping;
				if (sleeping)
				{
					BroadcastPlayerEvent(PlayerEventRules.SleepDelivery(PeerName(l), SleepingCount(), ActivePlayerCount()));
				}
			}
			break;
		case PlayerEventKind.Death:
		{
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (!(l.LastDeathAt > 0f) || !(realtimeSinceStartup - l.LastDeathAt < 2f))
			{
				l.LastDeathAt = realtimeSinceStartup;
				l.Sleeping = false;
				BroadcastPlayerEvent(PlayerEventRules.DeathDelivery(PeerName(l), hitType, attacker));
			}
			break;
		}
		}
	}

	private void OnEventDeliver(Link l, string packet)
	{
		if (CanReceive(l) && PlayerEventRules.ReadDelivery(packet, out var kind, out var name, out var sleeping, out var total, out var hitType, out var attacker))
		{
			switch (kind)
			{
			case PlayerEventKind.Sleep:
				AddLine(PlayerEventRules.SleepLine(name, sleeping, total, Spanish()));
				break;
			case PlayerEventKind.Death:
				AddLine(PlayerEventRules.DeathLine(name, hitType, DeathName.Display(attacker, LocalizeName), Spanish()));
				break;
			}
		}
	}

	private void BroadcastPlayerEvent(string packet)
	{
		Link[] array = links.Values.ToArray();
		foreach (Link link in array)
		{
			if (link.Ready && Active(link))
			{
				try
				{
					Send(link, "CDC0112_EventDeliver", packet);
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_PLAYER_EVENT_SEND: " + ex.GetType().Name));
				}
			}
		}
	}

	private string PeerName(Link l)
	{
		if (!l.Local)
		{
			return Rules.Clean((string)Get(l.Peer, "m_playerName"), 64);
		}
		return l.OwnPingName;
	}

	private int SleepingCount()
	{
		return links.Values.Count((Link x) => x.Ready && x.Sleeping && Active(x));
	}

	private int ActivePlayerCount()
	{
		return links.Values.Count((Link x) => x.Ready && Active(x));
	}

	private void ConfigureMessageSound()
	{
		messageSoundEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Notifications", "MessageSoundEnabled", true, "Subtle sound for incoming Valheim Chat Plus global messages and whispers. Restart the client after changing this setting.");
		messageSoundVolume = ((BaseUnityPlugin)this).Config.Bind<float>("Notifications", "MessageSoundVolume", 0.2f, "Notification volume, 0 to 1, additionally controlled by the game's GUI/SFX mixer. Default: 0.20.");
	}

	private void PrepareMessageSound()
	{
		//IL_01f2: Unknown result type (might be due to invalid IL or missing references)
		//IL_01fc: Expected O, but got Unknown
		if (soundAttempted || !messageSoundEnabled.Value)
		{
			return;
		}
		soundAttempted = true;
		string text = "types";
		try
		{
			Type type = AccessTools.TypeByName("UnityEngine.AudioClip");
			Type type2 = AccessTools.TypeByName("UnityEngine.AudioSource");
			Type type3 = AccessTools.TypeByName("AudioMan");
			if (type == null || type2 == null)
			{
				throw new MissingMemberException("Unity audio types unavailable.");
			}
			text = "game mixer";
			object obj = ((type3 == null) ? null : Instance(type3));
			object obj2 = ((obj == null) ? null : AccessTools.Field(type3, "m_guiMixer")?.GetValue(obj));
			if (obj2 == null && type3 != null)
			{
				MethodInfo methodInfo = AccessTools.Method(type3, "GetSFXVolume", Type.EmptyTypes, (Type[])null);
				if (methodInfo != null && methodInfo.IsStatic && methodInfo.ReturnType == typeof(float))
				{
					gameSfxVolume = Expression.Lambda<Func<float>>(Expression.Call(methodInfo), Array.Empty<ParameterExpression>()).Compile();
				}
			}
			text = "embedded WAV";
			float[] array;
			int frequency;
			using (Stream stream = typeof(Plugin).Assembly.GetManifestResourceStream("CrossplayDirectChat.message.wav"))
			{
				array = MessageWave.Read(stream, out frequency);
			}
			text = "clip";
			MethodInfo methodInfo2 = AccessTools.Method(type, "Create", new Type[5]
			{
				typeof(string),
				typeof(int),
				typeof(int),
				typeof(int),
				typeof(bool)
			}, (Type[])null);
			if (methodInfo2 == null)
			{
				throw new MissingMethodException("AudioClip.Create");
			}
			messageClip = (Object)methodInfo2.Invoke(null, new object[5] { "Valheim Chat Plus message", array.Length, 1, frequency, false });
			MethodInfo methodInfo3 = AccessTools.Method(type, "SetData", new Type[2]
			{
				typeof(float[]),
				typeof(int)
			}, (Type[])null);
			if (methodInfo3 == null)
			{
				throw new MissingMethodException("AudioClip.SetData");
			}
			if (!(bool)methodInfo3.Invoke(messageClip, new object[2] { array, 0 }))
			{
				throw new InvalidOperationException("Audio data rejected.");
			}
			text = "source";
			messageSource = (Object)(object)((Component)this).gameObject.AddComponent(type2);
			SetAudioProperty(type2, "playOnAwake", false);
			SetAudioProperty(type2, "loop", false);
			SetAudioProperty(type2, "spatialBlend", 0f);
			SetAudioProperty(type2, "volume", 1f);
			if (obj2 != null)
			{
				SetAudioProperty(type2, "outputAudioMixerGroup", obj2);
			}
			text = "playback delegates";
			ConstantExpression constantExpression = Expression.Constant(messageSource, type2);
			ParameterExpression parameterExpression = Expression.Parameter(typeof(float), "volume");
			MethodInfo methodInfo4 = AccessTools.Method(type2, "PlayOneShot", new Type[2]
			{
				type,
				typeof(float)
			}, (Type[])null);
			PropertyInfo propertyInfo = AccessTools.Property(type2, "isPlaying");
			if (methodInfo4 == null || propertyInfo == null)
			{
				throw new MissingMemberException("AudioSource playback members");
			}
			playMessageClip = Expression.Lambda<Action<float>>(Expression.Call(constantExpression, methodInfo4, Expression.Constant(messageClip, type), parameterExpression), new ParameterExpression[1] { parameterExpression }).Compile();
			messageClipPlaying = Expression.Lambda<Func<bool>>(Expression.Property(constantExpression, propertyInfo), Array.Empty<ParameterExpression>()).Compile();
			((BaseUnityPlugin)this).Logger.LogInfo((object)("DIRECT_MESSAGE_SOUND_READY 0.1.16 route=" + ((obj2 == null) ? "default+sfx-volume" : "gui-mixer")));
		}
		catch (Exception ex)
		{
			ReleaseMessageSound();
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_MESSAGE_SOUND_DISABLED stage=" + text + " error=" + ex.GetType().Name + "; chat remains available."));
		}
	}

	private void SetAudioProperty(Type sourceType, string name, object value)
	{
		PropertyInfo propertyInfo = AccessTools.Property(sourceType, name);
		if (propertyInfo == null || propertyInfo.SetMethod == null)
		{
			throw new MissingMemberException("AudioSource." + name);
		}
		propertyInfo.SetValue(messageSource, value);
	}

	private void PlayMessageSound()
	{
		if (playMessageClip == null || !messageSoundEnabled.Value)
		{
			return;
		}
		float num = MessageSoundGate.Volume(messageSoundVolume.Value);
		if (num <= 0f)
		{
			return;
		}
		try
		{
			if (!messageClipPlaying() && soundGate.Allow(Time.realtimeSinceStartup))
			{
				if (gameSfxVolume != null)
				{
					num = MessageSoundGate.Volume(num * gameSfxVolume());
				}
				if (!(num <= 0f))
				{
					playMessageClip(num);
				}
			}
		}
		catch (Exception ex)
		{
			ReleaseMessageSound();
			((BaseUnityPlugin)this).Logger.LogWarning((object)("DIRECT_MESSAGE_SOUND_DISABLED: " + ex.GetType().Name));
		}
	}

	private void ReleaseMessageSound()
	{
		playMessageClip = null;
		messageClipPlaying = null;
		gameSfxVolume = null;
		if (messageSource != (Object)null)
		{
			Object.Destroy(messageSource);
		}
		if (messageClip != (Object)null)
		{
			Object.Destroy(messageClip);
		}
		messageSource = (messageClip = null);
	}
}
internal static class Texts
{
	private static readonly Dictionary<string, string[]> Entries = new Dictionary<string, string[]>(StringComparer.Ordinal)
	{
		{
			"host.error",
			new string[2] { "Local mod chat could not start. Check LogOutput.log; ordinary text uses vanilla.", "No se pudo iniciar el chat local del mod. Revisá LogOutput.log; el texto normal usa vanilla." }
		},
		{
			"color.error",
			new string[2] { "The server could not load or save your color. Check the server log.", "El servidor no pudo cargar o guardar tu color. Revisá el log del servidor." }
		},
		{
			"welcome",
			new string[2] { "Connected. Chat: {0}. /chathelp for commands.", "Conectado. Chat: {0}. /chathelp para ver los comandos." }
		},
		{
			"mode.global",
			new string[2] { "Global", "Global" }
		},
		{
			"mode.vanilla",
			new string[2] { "Vanilla", "Vanilla" }
		},
		{
			"mode.status",
			new string[2] { "Preferred chat: {0}. Active chat: {1}.", "Chat preferido: {0}. Chat activo: {1}." }
		},
		{
			"mode.usage",
			new string[2] { "Usage: /chatmode [global|vanilla|status]. Without an argument, toggles and saves.", "Uso: /chatmode [global|vanilla|status]. Sin argumento alterna y guarda." }
		},
		{
			"mode.save_error",
			new string[2] { "Could not save the chat mode. Check BepInEx/config permissions.", "No se pudo guardar el modo de chat. Revisá los permisos de BepInEx/config." }
		},
		{
			"mode.saved",
			new string[2] { "Preferred chat saved: {0}. Active chat: {1}. Other mod features remain enabled.", "Chat preferido guardado: {0}. Chat activo: {1}. Las demás funciones del mod siguen activas." }
		},
		{
			"fallback",
			new string[2] { "Global chat is unavailable. Using vanilla until a compatible server connects.", "El chat global no está disponible. Se usa vanilla hasta conectar con un servidor compatible." }
		},
		{
			"action.error",
			new string[2] { "The action could not be completed. Check LogOutput.log.", "No se completó la acción. Revisá LogOutput.log." }
		},
		{
			"offline",
			new string[2] { "Not connected to Valheim Chat Plus. The host/server needs a compatible mod version; try again shortly.", "Sin conexión a Valheim Chat Plus. El anfitrión/servidor necesita una versión compatible del mod; intentá de nuevo en unos segundos." }
		},
		{
			"mute.usage",
			new string[2] { "Usage: /cmute ID (see /cplayers). Repeat to unmute.", "Uso: /cmute ID (consultá /cplayers). Repetir para desbloquear." }
		},
		{
			"mute.off",
			new string[2] { "Unmuted {0}.", "Desbloqueado {0}." }
		},
		{
			"mute.on",
			new string[2] { "Muted {0}.", "Silenciado {0}." }
		},
		{
			"global.usage",
			new string[2] { "Usage: /c message (maximum 400 characters; global).", "Uso: /c mensaje (máximo 400 caracteres; global)." }
		},
		{
			"global.server",
			new string[2] { "Global chat requires the mod on the host/server and clients.", "El chat global requiere el mod en el anfitrión/servidor y en los clientes." }
		},
		{
			"color.admin",
			new string[2] { "Only server administrators can change colors.", "Solo los administradores del servidor pueden cambiar colores." }
		},
		{
			"color.saved",
			new string[2] { "Color saved: {0} = {1}. Applies to new messages.", "Color guardado: {0} = {1}. Se aplica a los mensajes nuevos." }
		},
		{
			"color.failed",
			new string[2] { "Could not complete the change. Check the server log; no new color was confirmed.", "No se pudo completar el cambio. Revisá el log del servidor; no se confirmó un color nuevo." }
		},
		{
			"color.usage",
			new string[2] { "Usage: /chatcolor set NAME_OR_ID #RRGGBB | /chatcolor reset NAME_OR_ID", "Uso: /chatcolor set NOMBRE_O_ID #RRGGBB | /chatcolor reset NOMBRE_O_ID" }
		},
		{
			"color.unknown",
			new string[2] { "Unknown ID. Copy the stable ID from /cplayers.", "ID desconocido. Copiá el ID estable de /cplayers." }
		},
		{
			"color.invalid",
			new string[2] { "Invalid color. Use #RRGGBB, for example #FF8800.", "Color inválido. Usá #RRGGBB, por ejemplo #FF8800." }
		},
		{
			"color.used",
			new string[2] { "That color is already assigned to another player.", "Ese color ya está asignado a otro jugador." }
		},
		{
			"color.exhausted",
			new string[2] { "No unused color is available.", "No hay un color libre disponible." }
		},
		{
			"name.duplicate",
			new string[2] { "Duplicate name. Use the stable ID from /cplayers.", "Nombre duplicado. Usá el ID estable de /cplayers." }
		},
		{
			"name.missing",
			new string[2] { "Player not found. Use their exact connected name or the ID from /cplayers.", "Jugador no encontrado. Usá su nombre exacto conectado o el ID de /cplayers." }
		},
		{
			"name.quotes",
			new string[2] { "Invalid quotes. Example: /chatcolor set \"Player name\" #FF69B4", "Comillas inválidas. Ejemplo: /chatcolor set \"Nombre del jugador\" #FF69B4" }
		},
		{
			"name.quote_full",
			new string[2] { "Put the full name in quotes.", "Encerrá el nombre completo entre comillas." }
		},
		{
			"name.empty",
			new string[2] { "The name cannot be empty.", "El nombre no puede estar vacío." }
		},
		{
			"players.row",
			new string[2] { "{0} — ID: {1} — {2} — /cmute {3}", "{0} — ID: {1} — {2} — /cmute {3}" }
		},
		{
			"whisper.replyusage",
			new string[2] { "Usage: /r message (maximum 400 characters).", "Uso: /r mensaje (máximo 400 caracteres)." }
		},
		{
			"whisper.noreply",
			new string[2] { "You have not received a whisper on this connection yet.", "Todavía no recibiste un susurro en esta conexión." }
		},
		{
			"whisper.usage",
			new string[2] { "Usage: /w \"Player name\" message (maximum 400 characters).", "Uso: /w \"Nombre del jugador\" mensaje (máximo 400 caracteres)." }
		},
		{
			"whisper.target",
			new string[2] { "Recipient unavailable or ambiguous. Use the full name or an ID from /cplayers.", "Destinatario ausente o ambiguo. Usá el nombre completo o el ID de /cplayers." }
		},
		{
			"whisper.offline",
			new string[2] { "Not connected to Valheim Chat Plus. Whisper not sent.", "Sin conexión a Valheim Chat Plus. No se envió el susurro." }
		},
		{
			"whisper.muted",
			new string[2] { "You muted this player. Unmute with /cmute before whispering.", "Tenés silenciado a ese jugador. Desbloquealo con /cmute antes de escribirle." }
		},
		{
			"whisper.send",
			new string[2] { "Could not confirm whisper submission.", "No se pudo confirmar el envío del susurro." }
		},
		{
			"label.server",
			new string[2] { "Server", "Servidor" }
		},
		{
			"label.whisper",
			new string[2] { "Whisper", "Susurro" }
		},
		{
			"presence.join",
			new string[2] { "{0} joined the server.", "{0} se conectó." }
		},
		{
			"presence.leave",
			new string[2] { "{0} left the server.", "{0} se desconectó." }
		},
		{
			"sleep",
			new string[2] { "{0} went to bed. ({1}/{2} players sleeping)", "{0} se acostó. ({1}/{2} jugadores durmiendo)" }
		},
		{
			"help.1",
			new string[2] { "Normal text uses your active chat mode. Global messages reach all connected mod users.", "El texto normal usa el modo activo. El global llega a todos los usuarios conectados con el mod." }
		},
		{
			"help.2",
			new string[2] { "/chatmode [global|vanilla|status] — switch chat mode; other features stay enabled.", "/chatmode [global|vanilla|status] — cambiar el modo; las demás funciones siguen activas." }
		},
		{
			"help.3",
			new string[2] { "/c message — send global; /w \"Name\" message — whisper; /r message — reply.", "/c mensaje — enviar global; /w \"Nombre\" mensaje — susurrar; /r mensaje — responder." }
		},
		{
			"help.4",
			new string[2] { "Press Tab after /w to complete a player name. Use quotes for names with spaces.", "Presioná Tab después de /w para completar un nombre. Usá comillas para nombres con espacios." }
		},
		{
			"help.5",
			new string[2] { "/cplayers — list players and IDs; /cmute ID — mute/unmute mod messages for this connection.", "/cplayers — listar jugadores e IDs; /cmute ID — silenciar/desbloquear mensajes del mod durante esta conexión." }
		},
		{
			"help.6",
			new string[2] { "Admins: /chatcolor set \"Name\" #RRGGBB | /chatcolor reset \"Name\". Stable IDs also work.", "Admins: /chatcolor set \"Nombre\" #RRGGBB | /chatcolor reset \"Nombre\". También admite IDs estables." }
		},
		{
			"help.7",
			new string[2] { "Pings and join/leave, death and sleep notices stay enabled in both modes.", "Los pings y avisos de conexión, desconexión, muerte y descanso siguen activos en ambos modos." }
		},
		{
			"death.0",
			new string[2] { "died.", "murió." }
		},
		{
			"death.3",
			new string[2] { "died from a fall.", "murió por una caída." }
		},
		{
			"death.4",
			new string[2] { "drowned.", "se ahogó." }
		},
		{
			"death.5",
			new string[2] { "burned to death.", "murió quemado." }
		},
		{
			"death.6",
			new string[2] { "froze to death.", "murió congelado." }
		},
		{
			"death.7",
			new string[2] { "died from poison.", "murió envenenado." }
		},
		{
			"death.8",
			new string[2] { "died in the water.", "murió en el agua." }
		},
		{
			"death.9",
			new string[2] { "died from smoke inhalation.", "murió por inhalación de humo." }
		},
		{
			"death.10",
			new string[2] { "fell off the edge of the world.", "cayó del borde del mundo." }
		},
		{
			"death.11",
			new string[2] { "died from an impact.", "murió por un impacto." }
		},
		{
			"death.12",
			new string[2] { "died in a cart accident.", "murió en un accidente con un carro." }
		},
		{
			"death.13",
			new string[2] { "was crushed by a tree.", "murió aplastado por un árbol." }
		},
		{
			"death.14",
			new string[2] { "died from self-inflicted damage.", "murió por daño propio." }
		},
		{
			"death.15",
			new string[2] { "died from structural damage.", "murió por daño estructural." }
		},
		{
			"death.16",
			new string[2] { "was killed by a turret.", "murió por una torreta." }
		},
		{
			"death.17",
			new string[2] { "died in a boat accident.", "murió en un accidente con una embarcación." }
		},
		{
			"death.18",
			new string[2] { "was impaled by a stalactite.", "fue empalado por una estalactita." }
		},
		{
			"death.19",
			new string[2] { "was killed by a catapult impact.", "murió por un impacto de catapulta." }
		},
		{
			"death.20",
			new string[2] { "died in cinder fire.", "murió por fuego de ceniza." }
		},
		{
			"death.21",
			new string[2] { "died in the Ashlands ocean.", "murió en el océano de Ashlands." }
		},
		{
			"death.22",
			new string[2] { "died in lava.", "murió en la lava." }
		},
		{
			"death.23",
			new string[2] { "died in the incinerator.", "murió en el incinerador." }
		},
		{
			"death.24",
			new string[2] { "was crushed by a drawbridge.", "murió aplastado por un puente levadizo." }
		},
		{
			"death.1",
			new string[2] { "was killed by an enemy.", "murió a manos de un enemigo." }
		},
		{
			"death.1.named",
			new string[2] { "was killed by {0}.", "murió a manos de {0}." }
		},
		{
			"death.2",
			new string[2] { "died in PvP.", "murió en PvP." }
		},
		{
			"death.2.named",
			new string[2] { "was killed by {0} (PvP).", "murió a manos de {0} (PvP)." }
		}
	};

	internal static IEnumerable<string> Keys => Entries.Keys;

	internal static bool Has(string key)
	{
		if (key != null)
		{
			return Entries.ContainsKey(key);
		}
		return false;
	}

	internal static bool IsSpanish(string language)
	{
		if (string.IsNullOrWhiteSpace(language))
		{
			return false;
		}
		language = language.Trim();
		if (!language.StartsWith("Spanish", StringComparison.OrdinalIgnoreCase) && !language.StartsWith("Espa", StringComparison.OrdinalIgnoreCase) && !language.Equals("es", StringComparison.OrdinalIgnoreCase) && !language.StartsWith("es-", StringComparison.OrdinalIgnoreCase))
		{
			return language.StartsWith("es_", StringComparison.OrdinalIgnoreCase);
		}
		return true;
	}

	internal static string Get(string key, bool spanish, params object[] args)
	{
		if (!Entries.TryGetValue(key, out var value))
		{
			value = Entries["action.error"];
		}
		string text = value[spanish ? 1u : 0u];
		if (string.IsNullOrEmpty(text))
		{
			text = value[0];
		}
		return string.Format(CultureInfo.InvariantCulture, text, args);
	}
}
internal static class NoticePacket
{
	internal static string Pack(string key, params string[] args)
	{
		if (!Texts.Has(key) || args.Length > 4)
		{
			throw new ArgumentException("Invalid notice key/arguments.");
		}
		string text = key;
		foreach (string s in args)
		{
			text = text + "\n" + Rules.Clean(s, 160);
		}
		return text;
	}

	internal static bool TryFormat(string packet, bool spanish, out string text)
	{
		text = null;
		if (packet == null || packet.Length > 800)
		{
			return false;
		}
		string[] array = packet.Split('\n');
		if (array.Length > 5 || !Texts.Has(array[0]))
		{
			return false;
		}
		object[] array2 = new object[array.Length - 1];
		for (int i = 1; i < array.Length; i++)
		{
			string text2 = Rules.Clean(array[i], 160);
			if (text2 != array[i])
			{
				return false;
			}
			array2[i - 1] = text2;
		}
		try
		{
			text = Texts.Get(array[0], spanish, array2);
			return true;
		}
		catch (FormatException)
		{
			return false;
		}
	}
}
internal static class DeathName
{
	internal static string Pack(string name, bool token)
	{
		return (token ? "K:" : "N:") + Rules.Clean(name, 78);
	}

	internal static string Display(string value, Func<string, string> localize)
	{
		if (string.IsNullOrEmpty(value))
		{
			return "";
		}
		if (value.StartsWith("N:", StringComparison.Ordinal))
		{
			return Rules.Clean(value.Substring(2), 78);
		}
		if (value.StartsWith("K:", StringComparison.Ordinal))
		{
			string text = value.Substring(2);
			if (!text.StartsWith("$", StringComparison.Ordinal))
			{
				return "";
			}
			try
			{
				string text2 = Rules.Clean(localize(text), 80);
				return (text2.Length == 0 || text2 == text || text2.IndexOf('$') >= 0 || text2[0] == '[') ? "" : text2;
			}
			catch
			{
				return "";
			}
		}
		return "";
	}
}
internal enum InputRoute
{
	Vanilla,
	Command,
	Global
}
internal static class Rules
{
	internal static InputRoute Route(string text, bool global)
	{
		if (string.IsNullOrWhiteSpace(text))
		{
			return InputRoute.Vanilla;
		}
		if (WhisperRules.IsCommand(text) || ReplyState.IsCommand(text))
		{
			return InputRoute.Command;
		}
		string text2 = text.TrimStart();
		string[] array = new string[6] { "/chathelp", "/chatcolor", "/chatmode", "/cplayers", "/cmute", "/c" };
		foreach (string text3 in array)
		{
			if (text2.Equals(text3, StringComparison.OrdinalIgnoreCase) || text2.StartsWith(text3 + " ", StringComparison.OrdinalIgnoreCase))
			{
				return InputRoute.Command;
			}
		}
		if (text2.StartsWith("/", StringComparison.Ordinal))
		{
			return InputRoute.Vanilla;
		}
		if (!global)
		{
			return InputRoute.Vanilla;
		}
		return InputRoute.Global;
	}

	internal static InputRoute ResolveAvailability(InputRoute route, bool available)
	{
		if (route != InputRoute.Global || available)
		{
			return route;
		}
		return InputRoute.Vanilla;
	}

	internal static string Clean(string s, int limit)
	{
		if (s == null)
		{
			return "";
		}
		StringBuilder stringBuilder = new StringBuilder();
		foreach (char c in s)
		{
			if (stringBuilder.Length >= limit)
			{
				break;
			}
			if (!char.IsControl(c) && !char.IsSurrogate(c))
			{
				stringBuilder.Append(c switch
				{
					'>' => '›', 
					'<' => '‹', 
					_ => c, 
				});
			}
		}
		return stringBuilder.ToString().Trim();
	}
}
internal sealed class Rate
{
	private double tokens = 5.0;

	private double previous;

	internal bool Allow(double now)
	{
		tokens = Math.Min(5.0, tokens + Math.Max(0.0, now - previous));
		previous = now;
		if (tokens < 1.0)
		{
			return false;
		}
		tokens -= 1.0;
		return true;
	}
}
internal static class ColorRules
{
	internal static bool Identity(string id)
	{
		if (string.IsNullOrEmpty(id) || id.Length > 160 || !id.Contains("_"))
		{
			return false;
		}
		foreach (char c in id)
		{
			if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' && c != '-' && c != '.')
			{
				return false;
			}
		}
		return true;
	}

	internal static string TransportIdentity(string socketType, string host)
	{
		if (socketType == "ZSteamSocket" && ulong.TryParse(host, NumberStyles.None, CultureInfo.InvariantCulture, out var result))
		{
			host = "Steam_" + result.ToString(CultureInfo.InvariantCulture);
		}
		if (!Identity(host))
		{
			throw new FormatException("Stable transport identity unavailable; no character-name/UID fallback.");
		}
		return host;
	}

	internal static bool Color(string value)
	{
		if (value == null || value.Length != 7 || value[0] != '#')
		{
			return false;
		}
		for (int i = 1; i < 7; i++)
		{
			if (!Uri.IsHexDigit(value[i]))
			{
				return false;
			}
		}
		return true;
	}
}
internal sealed class ColorStore
{
	private const string Header = "CDC_COLORS_1";

	private static readonly string[] Palette = new string[12]
	{
		"#7DD3FC", "#FDE68A", "#C4B5FD", "#86EFAC", "#FDA4AF", "#FDBA74", "#67E8F9", "#F0ABFC", "#BEF264", "#A5B4FC",
		"#F9A8D4", "#D6D3D1"
	};

	private readonly string path;

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

	private ColorStore(string path)
	{
		this.path = path;
	}

	internal static ColorStore Load(string path)
	{
		ColorStore colorStore = new ColorStore(path);
		if (!File.Exists(path))
		{
			if (Directory.Exists(path))
			{
				throw new IOException("Color file path is a directory.");
			}
			return colorStore;
		}
		using StreamReader streamReader = new StreamReader(path, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), detectEncodingFromByteOrderMarks: true);
		if (streamReader.ReadLine() != "CDC_COLORS_1")
		{
			throw new FormatException("Invalid colors header; original file preserved.");
		}
		string text;
		while ((text = streamReader.ReadLine()) != null)
		{
			string[] array = text.Split('\t');
			if (array.Length != 2 || !ColorRules.Identity(array[0]) || !ColorRules.Color(array[1]) || colorStore.entries.ContainsKey(array[0]))
			{
				throw new FormatException("Invalid/duplicate color entry; original file preserved.");
			}
			colorStore.entries.Add(array[0], array[1].ToUpperInvariant());
		}
		return colorStore;
	}

	internal string GetOrAssign(string id)
	{
		if (!ColorRules.Identity(id))
		{
			throw new FormatException("Invalid transport identity.");
		}
		if (entries.TryGetValue(id, out var value))
		{
			return value;
		}
		HashSet<string> used = new HashSet<string>(entries.Values, StringComparer.OrdinalIgnoreCase);
		string text = Palette.FirstOrDefault((string item) => !used.Contains(item));
		if (text == null)
		{
			uint num = 2166136261u;
			foreach (char c in id)
			{
				num = (num ^ c) * 16777619;
			}
			for (uint num3 = 0u; num3 < 100000; num3++)
			{
				uint num4 = num + (uint)((int)num3 * -1640531535);
				string text2 = "#" + ((num4 & 0x7F) + 96).ToString("X2") + (((num4 >> 8) & 0x7F) + 96).ToString("X2") + (((num4 >> 16) & 0x7F) + 96).ToString("X2");
				if (!used.Contains(text2))
				{
					text = text2;
					break;
				}
			}
			if (text == null)
			{
				throw new InvalidOperationException("color.exhausted");
			}
		}
		entries.Add(id, text);
		try
		{
			Save();
			return text;
		}
		catch
		{
			entries.Remove(id);
			throw;
		}
	}

	internal string AdminCommand(bool authorized, string command, out string target, Func<string, string> resolveName = null)
	{
		target = null;
		if (!authorized)
		{
			throw new UnauthorizedAccessException();
		}
		if (command == null || command.Length > 240)
		{
			throw new FormatException("color.usage");
		}
		string[] array = NameTargets.Words(command);
		if (array.Length < 3 || !array[0].Equals("/chatcolor", StringComparison.OrdinalIgnoreCase) || ((!array[1].Equals("set", StringComparison.OrdinalIgnoreCase) || array.Length != 4) && (!array[1].Equals("reset", StringComparison.OrdinalIgnoreCase) || array.Length != 3)))
		{
			throw new FormatException("color.usage");
		}
		target = array[2];
		if (!entries.ContainsKey(target) && resolveName != null)
		{
			target = resolveName(target);
		}
		if (!ColorRules.Identity(target) || !entries.TryGetValue(target, out var value))
		{
			throw new InvalidOperationException("color.unknown");
		}
		if (array.Length == 3)
		{
			entries.Remove(target);
			try
			{
				return GetOrAssign(target);
			}
			catch
			{
				entries[target] = value;
				throw;
			}
		}
		if (!ColorRules.Color(array[3]))
		{
			throw new FormatException("color.invalid");
		}
		string color = array[3].ToUpperInvariant();
		string targetId = target;
		if (entries.Any((KeyValuePair<string, string> p) => p.Key != targetId && p.Value.Equals(color, StringComparison.OrdinalIgnoreCase)))
		{
			throw new InvalidOperationException("color.used");
		}
		if (color == value)
		{
			return value;
		}
		entries[target] = color;
		try
		{
			Save();
		}
		catch
		{
			entries[target] = value;
			throw;
		}
		return color;
	}

	private void Save()
	{
		Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
		string sourceFileName = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
		try
		{
			using (FileStream fileStream = new FileStream(sourceFileName, FileMode.CreateNew, FileAccess.Write, FileShare.None))
			{
				using (StreamWriter streamWriter = new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 1024, leaveOpen: true))
				{
					streamWriter.WriteLine("CDC_COLORS_1");
					foreach (KeyValuePair<string, string> item in entries.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> p) => p.Key, StringComparer.Ordinal))
					{
						streamWriter.WriteLine(item.Key + "\t" + item.Value);
					}
					streamWriter.Flush();
				}
				fileStream.Flush(flushToDisk: true);
			}
			if (File.Exists(path))
			{
				File.Replace(sourceFileName, path, path + ".bak");
			}
			else
			{
				File.Move(sourceFileName, path);
			}
		}
		finally
		{
			try
			{
				if (File.Exists(sourceFileName))
				{
					File.Delete(sourceFileName);
				}
			}
			catch
			{
			}
		}
	}
}
internal static class ColorPacket
{
	internal static string Pack(string id, string name, string color, string text)
	{
		return id + "\n" + name + "\n" + color + "\n" + text;
	}

	internal static bool TryRead(string packet, out string id, out string name, out string color, out string text)
	{
		id = (name = (color = (text = null)));
		if (packet == null || packet.Length > 520)
		{
			return false;
		}
		string[] array = packet.Split(new char[1] { '\n' }, 4);
		if (array.Length != 4 || !long.TryParse(array[0], out var _) || array[1].Length > 64 || !ColorRules.Color(array[2]) || array[3].Length > 400)
		{
			return false;
		}
		id = array[0];
		name = Rules.Clean(array[1], 64);
		color = array[2];
		text = Rules.Clean(array[3], 400);
		return text.Length != 0;
	}
}
internal static class NameTargets
{
	internal static string Resolve(string name, IEnumerable<KeyValuePair<string, string>> players)
	{
		string text = null;
		foreach (KeyValuePair<string, string> player in players)
		{
			if (player.Value.Equals(name, StringComparison.OrdinalIgnoreCase))
			{
				if (text != null)
				{
					throw new InvalidOperationException("name.duplicate");
				}
				text = player.Key;
			}
		}
		if (text == null)
		{
			throw new InvalidOperationException("name.missing");
		}
		return text;
	}

	internal static string[] Words(string text)
	{
		List<string> list = new List<string>();
		int num = 0;
		while (num < text.Length)
		{
			if (char.IsWhiteSpace(text[num]))
			{
				num++;
				continue;
			}
			StringBuilder stringBuilder = new StringBuilder();
			if (text[num] == '"')
			{
				num++;
				bool flag = false;
				char c;
				for (; num < text.Length; stringBuilder.Append(c))
				{
					c = text[num++];
					switch (c)
					{
					case '"':
						break;
					case '\\':
						if (num < text.Length && (text[num] == '"' || text[num] == '\\'))
						{
							c = text[num++];
						}
						continue;
					default:
						continue;
					}
					flag = true;
					break;
				}
				if (!flag || (num < text.Length && !char.IsWhiteSpace(text[num])))
				{
					throw new FormatException("name.quotes");
				}
			}
			else
			{
				while (num < text.Length && !char.IsWhiteSpace(text[num]))
				{
					if (text[num] == '"')
					{
						throw new FormatException("name.quote_full");
					}
					stringBuilder.Append(text[num++]);
				}
			}
			if (stringBuilder.Length == 0)
			{
				throw new FormatException("name.empty");
			}
			list.Add(stringBuilder.ToString());
		}
		return list.ToArray();
	}
}
internal static class PingRules
{
	internal static string Welcome(long uid, string name)
	{
		return "0110\n" + uid.ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(name, 64);
	}

	internal static bool ReadWelcome(string s, out long uid, out string name)
	{
		uid = 0L;
		name = null;
		if (s == null || s.Length > 90)
		{
			return false;
		}
		string[] array = s.Split('\n');
		if (array.Length != 3 || array[0] != "0110" || array[2].Length > 64 || !long.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out uid))
		{
			return false;
		}
		name = Rules.Clean(array[2], 64);
		return true;
	}

	internal static bool Coordinate(string s, out float value)
	{
		if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value) && !float.IsNaN(value) && !float.IsInfinity(value))
		{
			return Math.Abs(value) <= 20000f;
		}
		return false;
	}

	internal static string Pack(float x, float y, float z)
	{
		return x.ToString("R", CultureInfo.InvariantCulture) + "\n" + y.ToString("R", CultureInfo.InvariantCulture) + "\n" + z.ToString("R", CultureInfo.InvariantCulture);
	}

	internal static bool Position(string s, out float x, out float y, out float z)
	{
		x = (y = (z = 0f));
		if (s == null || s.Length > 120)
		{
			return false;
		}
		string[] array = s.Split('\n');
		if (array.Length == 3 && Coordinate(array[0], out x) && Coordinate(array[1], out y))
		{
			return Coordinate(array[2], out z);
		}
		return false;
	}

	internal static string Delivery(long uid, string name, float x, float y, float z)
	{
		return uid.ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(name, 64) + "\n" + Pack(x, y, z);
	}

	internal static bool Read(string s, out long uid, out string name, out float x, out float y, out float z)
	{
		uid = 0L;
		name = null;
		x = (y = (z = 0f));
		if (s == null || s.Length > 240)
		{
			return false;
		}
		string[] array = s.Split(new char[1] { '\n' }, 3);
		if (array.Length != 3 || !long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out uid) || array[1].Length > 64 || !Position(array[2], out x, out y, out z))
		{
			return false;
		}
		name = Rules.Clean(array[1], 64);
		return true;
	}
}
internal sealed class PingRate
{
	private double tokens = 3.0;

	private double previous;

	internal bool Allow(double now)
	{
		tokens = Math.Min(3.0, tokens + Math.Max(0.0, now - previous) * 0.5);
		previous = now;
		if (tokens < 1.0)
		{
			return false;
		}
		tokens -= 1.0;
		return true;
	}
}
internal sealed class WhisperPlayer
{
	internal string Id;

	internal string Name;

	internal long Uid;

	internal WhisperPlayer(string id, long uid, string name)
	{
		Id = id;
		Uid = uid;
		Name = name;
	}
}
internal static class WhisperRules
{
	internal const int MaxPlayers = 128;

	internal const int MaxRoster = 32768;

	internal static bool IsCommand(string text)
	{
		if (text == null)
		{
			return false;
		}
		string text2 = text.TrimStart();
		if (text2.Length >= 2 && text2[0] == '/' && (text2[1] == 'w' || text2[1] == 'W'))
		{
			if (text2.Length != 2)
			{
				return char.IsWhiteSpace(text2[2]);
			}
			return true;
		}
		return false;
	}

	internal static bool Parse(string text, out string target, out string message)
	{
		target = (message = null);
		if (!IsCommand(text) || text.Length > 700)
		{
			return false;
		}
		text = text.TrimStart();
		int i;
		for (i = 2; i < text.Length && char.IsWhiteSpace(text[i]); i++)
		{
		}
		if (i == text.Length)
		{
			return false;
		}
		StringBuilder stringBuilder = new StringBuilder();
		if (text[i] == '"')
		{
			i++;
			bool flag = false;
			char c;
			for (; i < text.Length; stringBuilder.Append(c))
			{
				c = text[i++];
				switch (c)
				{
				case '"':
					break;
				case '\\':
					if (i < text.Length && (text[i] == '"' || text[i] == '\\'))
					{
						c = text[i++];
					}
					continue;
				default:
					continue;
				}
				flag = true;
				break;
			}
			if (!flag || (i < text.Length && !char.IsWhiteSpace(text[i])))
			{
				return false;
			}
		}
		else
		{
			while (i < text.Length && !char.IsWhiteSpace(text[i]))
			{
				if (text[i] == '"')
				{
					return false;
				}
				stringBuilder.Append(text[i++]);
			}
		}
		target = stringBuilder.ToString();
		message = text.Substring(i).Trim();
		if (target.Length > 0 && target.Length <= 160 && message.Length > 0)
		{
			return message.Length <= 400;
		}
		return false;
	}

	internal static string Quote(string name)
	{
		return "\"" + name.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
	}

	internal static WhisperPlayer Resolve(string target, IReadOnlyList<WhisperPlayer> players)
	{
		WhisperPlayer whisperPlayer = null;
		foreach (WhisperPlayer player in players)
		{
			if (player.Id == target)
			{
				if (whisperPlayer != null)
				{
					return null;
				}
				whisperPlayer = player;
			}
		}
		if (whisperPlayer != null)
		{
			return whisperPlayer;
		}
		foreach (WhisperPlayer player2 in players)
		{
			if (player2.Name.Equals(target, StringComparison.OrdinalIgnoreCase))
			{
				if (whisperPlayer != null)
				{
					return null;
				}
				whisperPlayer = player2;
			}
		}
		return whisperPlayer;
	}

	internal static string Roster(IReadOnlyList<WhisperPlayer> players)
	{
		if (players.Count > 128)
		{
			throw new InvalidOperationException("Too many DirectChat players.");
		}
		StringBuilder stringBuilder = new StringBuilder();
		foreach (WhisperPlayer player in players)
		{
			if (stringBuilder.Length > 0)
			{
				stringBuilder.Append('\n');
			}
			stringBuilder.Append(player.Id).Append('\t').Append(player.Uid.ToString(CultureInfo.InvariantCulture))
				.Append('\t')
				.Append(Rules.Clean(player.Name, 64));
		}
		if (stringBuilder.Length > 32768)
		{
			throw new InvalidOperationException("Roster too large.");
		}
		return stringBuilder.ToString();
	}

	internal static bool ReadRoster(string packet, out WhisperPlayer[] players)
	{
		players = null;
		if (packet == null || packet.Length > 32768)
		{
			return false;
		}
		if (packet.Length == 0)
		{
			players = Array.Empty<WhisperPlayer>();
			return true;
		}
		string[] array = packet.Split('\n');
		if (array.Length > 128)
		{
			return false;
		}
		List<WhisperPlayer> list = new List<WhisperPlayer>();
		HashSet<string> hashSet = new HashSet<string>();
		HashSet<long> hashSet2 = new HashSet<long>();
		string[] array2 = array;
		for (int i = 0; i < array2.Length; i++)
		{
			string[] array3 = array2[i].Split('\t');
			if (array3.Length != 3 || array3[0].Length == 0 || array3[0].Length > 160 || Rules.Clean(array3[0], 160) != array3[0] || array3[2].Length > 64 || !long.TryParse(array3[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !hashSet.Add(array3[0]) || !hashSet2.Add(result))
			{
				return false;
			}
			list.Add(new WhisperPlayer(array3[0], result, Rules.Clean(array3[2], 64)));
		}
		players = list.ToArray();
		return true;
	}

	internal static string Submit(string id, string message)
	{
		return id + "\t" + Rules.Clean(message, 400);
	}

	internal static bool ReadSubmit(string packet, out string id, out string message)
	{
		id = (message = null);
		if (packet == null || packet.Length > 561)
		{
			return false;
		}
		string[] array = packet.Split('\t');
		if (array.Length != 2 || array[0].Length == 0 || array[0].Length > 160 || array[1].Length > 400)
		{
			return false;
		}
		id = array[0];
		message = Rules.Clean(array[1], 400);
		return message.Length > 0;
	}

	internal static string Delivery(WhisperPlayer from, WhisperPlayer to, string message)
	{
		return from.Uid.ToString(CultureInfo.InvariantCulture) + "\t" + Rules.Clean(from.Name, 64) + "\t" + to.Uid.ToString(CultureInfo.InvariantCulture) + "\t" + Rules.Clean(to.Name, 64) + "\t" + Rules.Clean(message, 400) + "\t" + from.Id;
	}

	internal static bool ReadDelivery(string packet, long ownUid, out long from, out string fromName, out long to, out string toName, out string message)
	{
		string senderId;
		return ReadDelivery(packet, ownUid, out from, out fromName, out to, out toName, out message, out senderId);
	}

	internal static bool ReadDelivery(string packet, long ownUid, out long from, out string fromName, out long to, out string toName, out string message, out string senderId)
	{
		senderId = null;
		from = (to = 0L);
		fromName = (toName = (message = null));
		if (packet == null || packet.Length > 741)
		{
			return false;
		}
		string[] array = packet.Split('\t');
		if (array.Length != 6 || !ColorRules.Identity(array[5]) || array[1].Length > 64 || array[3].Length > 64 || array[4].Length > 400 || !long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out from) || !long.TryParse(array[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out to) || (ownUid != from && ownUid != to))
		{
			return false;
		}
		senderId = array[5];
		fromName = Rules.Clean(array[1], 64);
		toName = Rules.Clean(array[3], 64);
		message = Rules.Clean(array[4], 400);
		return message.Length > 0;
	}

	internal static void Deliver<T>(T sender, T target, string packet, Action<T, string> send) where T : class
	{
		send(target, packet);
		if (sender != target)
		{
			send(sender, packet);
		}
	}
}
internal sealed class WhisperCompletion
{
	private string last;

	private string prefix;

	private string suffix;

	private int lastCaret;

	private int index;

	private string[] matches;

	internal void Reset()
	{
		last = (prefix = (suffix = null));
		matches = null;
		index = (lastCaret = 0);
	}

	internal bool Complete(string text, int caret, IReadOnlyList<WhisperPlayer> roster, out string result, out int position)
	{
		result = text;
		position = caret;
		if (text == last && caret == lastCaret && matches != null)
		{
			index = (index + 1) % matches.Length;
		}
		else
		{
			Reset();
			if (!WhisperRules.IsCommand(text) || text.TrimStart().Length < 3 || caret < 0 || caret > text.Length)
			{
				return false;
			}
			int i;
			for (i = text.Length - text.TrimStart().Length + 2; i < text.Length && char.IsWhiteSpace(text[i]); i++)
			{
			}
			if (caret < i)
			{
				return false;
			}
			string text2 = text.Substring(i, caret - i);
			string value = text2;
			if (text2.StartsWith("\"", StringComparison.Ordinal))
			{
				StringBuilder stringBuilder = new StringBuilder();
				for (int j = 1; j < text2.Length; j++)
				{
					char c = text2[j];
					switch (c)
					{
					case '"':
						return false;
					case '\\':
						if (j + 1 < text2.Length && (text2[j + 1] == '"' || text2[j + 1] == '\\'))
						{
							c = text2[++j];
						}
						break;
					}
					stringBuilder.Append(c);
				}
				value = stringBuilder.ToString();
			}
			else
			{
				string text3 = text2;
				foreach (char c2 in text3)
				{
					if (char.IsWhiteSpace(c2) || c2 == '"')
					{
						return false;
					}
				}
			}
			if (caret < text.Length && !char.IsWhiteSpace(text[caret]))
			{
				return false;
			}
			List<string> list = new List<string>();
			foreach (WhisperPlayer item in roster)
			{
				if (item.Name.Length != 0 && item.Name.StartsWith(value, StringComparison.OrdinalIgnoreCase))
				{
					list.Add((WhisperRules.Resolve(item.Name, roster) == item) ? WhisperRules.Quote(item.Name) : item.Id);
				}
			}
			if (list.Count == 0)
			{
				return false;
			}
			list.Sort(StringComparer.OrdinalIgnoreCase);
			matches = list.ToArray();
			prefix = text.Substring(0, i);
			suffix = text.Substring(caret);
		}
		string text4 = matches[index];
		result = prefix + text4 + ((suffix.Length == 0) ? " " : suffix);
		position = prefix.Length + text4.Length + ((suffix.Length == 0) ? 1 : 0);
		last = result;
		lastCaret = position;
		return true;
	}
}
internal enum PlayerEventKind
{
	None,
	Sleep,
	Death
}
internal static class PlayerEventRules
{
	internal const int MaxHitType = 24;

	internal static string SleepSubmit(bool sleeping)
	{
		return "S\n" + (sleeping ? "1" : "0");
	}

	internal static string DeathSubmit(int hitType, string attacker)
	{
		return "D\n" + NormalizeHitType(hitType).ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(attacker, 80);
	}

	internal static bool ReadSubmit(string packet, out PlayerEventKind kind, out bool sleeping, out int hitType, out string attacker)
	{
		kind = PlayerEventKind.None;
		sleeping = false;
		hitType = 0;
		attacker = "";
		if (packet == null || packet.Length > 128)
		{
			return false;
		}
		string[] array = packet.Split('\n');
		if (array.Length == 2 && array[0] == "S" && (array[1] == "0" || array[1] == "1"))
		{
			kind = PlayerEventKind.Sleep;
			sleeping = array[1] == "1";
			return true;
		}
		if (array.Length == 3 && array[0] == "D" && int.TryParse(array[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result) && result >= 0 && result <= 24)
		{
			string text = Rules.Clean(array[2], 80);
			if (text != array[2])
			{
				return false;
			}
			kind = PlayerEventKind.Death;
			hitType = result;
			attacker = text;
			return true;
		}
		return false;
	}

	internal static string SleepDelivery(string name, int sleeping, int total)
	{
		return "S\n" + Rules.Clean(name, 64) + "\n" + Math.Max(0, sleeping).ToString(CultureInfo.InvariantCulture) + "\n" + Math.Max(0, total).ToString(CultureInfo.InvariantCulture);
	}

	internal static string DeathDelivery(string victim, int hitType, string attacker)
	{
		return "D\n" + Rules.Clean(victim, 64) + "\n" + NormalizeHitType(hitType).ToString(CultureInfo.InvariantCulture) + "\n" + Rules.Clean(attacker, 80);
	}

	internal static bool ReadDelivery(string packet, out PlayerEventKind kind, out string name, out int sleeping, out int total, out int hitType, out string attacker)
	{
		kind = PlayerEventKind.None;
		name = "";
		sleeping = (total = (hitType = 0));
		attacker = "";
		if (packet == null || packet.Length > 240)
		{
			return false;
		}
		string[] array = packet.Split('\n');
		if (array.Length == 4 && array[0] == "S")
		{
			string text = Rules.Clean(array[1], 64);
			if (text.Length == 0 || text != array[1] || !int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out sleeping) || !int.TryParse(array[3], NumberStyles.None, CultureInfo.InvariantCulture, out total) || sleeping < 0 || total < 1 || sleeping > total || total > 1000)
			{
				return false;
			}
			kind = PlayerEventKind.Sleep;
			name = text;
			return true;
		}
		if (array.Length == 4 && array[0] == "D")
		{
			string text2 = Rules.Clean(array[1], 64);
			string text3 = Rules.Clean(array[3], 80);
			if (text2.Length == 0 || text2 != array[1] || text3 != array[3] || !int.TryParse(array[2], NumberStyles.None, CultureInfo.InvariantCulture, out hitType) || hitType < 0 || hitType > 24)
			{
				return false;
			}
			kind = PlayerEventKind.Death;
			name = text2;
			attacker = text3;
			return true;
		}
		return false;
	}

	internal static string SleepLine(string name, int sleeping, int total, bool spanish)
	{
		return "<color=#B8C7FF>[" + Texts.Get("label.server", spanish) + "] " + Texts.Get("sleep", spanish, Rules.Clean(name, 64), sleeping, total) + "</color>";
	}

	internal static string De