Decompiled source of ModNet v0.1.3

ModNet.dll

Decompiled 7 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Burst2Flame.Observable;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using ModsMenu;
using Steamworks;
using Steamworks.Data;
using StolenRealmModAPI;
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("ModNet")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+170bee66ef82872534ee913dc7aac45a42464837")]
[assembly: AssemblyProduct("ModNet")]
[assembly: AssemblyTitle("ModNet")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ModNet
{
	internal static class MenuCard
	{
		private const string Warn = "#E0A040";

		private const string Dim = "#9A8F85";

		internal static void Register()
		{
			ModsMenuApi.AddSection("com.helvetica.modnet", (Action<SectionBuilder>)Build);
		}

		private static void Build(SectionBuilder b)
		{
			b.AddSubheader("Players");
			if (!Session.IsMultiplayer)
			{
				b.AddLabel("Not in a multiplayer game.");
			}
			else
			{
				if (!Session.IsReady)
				{
					b.AddLabel("Connecting... (starts a few seconds after your characters appear)");
				}
				Dictionary<string, string> mine = Session.LocalMods();
				foreach (PlayerInfo player in Session.Players)
				{
					AddPlayer(b, player, mine);
				}
				b.AddLabel(string.Format("<color={0}>{1} Messages sent {2}, received {3}. ", "#9A8F85", Session.LobbyInfo(), Transport.Sent, Transport.Received) + "Updated when this card opens.</color>");
			}
			if (Plugin.TestTools.Value)
			{
				b.AddSubheader("Tests");
				b.AddButton("Ping", (Action)delegate
				{
					Notify(Session.Ping());
				}, "Measures the round trip to every modded player (BepInEx log).");
				b.AddButton("Payload size test", (Action)delegate
				{
					Notify(Session.SizeTest());
				}, "Sends 250 to 4000 characters and logs which sizes arrive on each PC.");
			}
		}

		private static void AddPlayer(SectionBuilder b, PlayerInfo p, Dictionary<string, string> mine)
		{
			string text = p.Name + (p.IsMe ? " (you)" : "");
			if (!p.HasModNet)
			{
				b.AddLabel(text + ": <color=#9A8F85>no ModNet (or not connected yet). They see the vanilla game.</color>");
				return;
			}
			List<string> list = new List<string>();
			IEnumerable<string> keys = p.Mods.Keys;
			IEnumerable<string> second;
			if (!p.IsMe)
			{
				IEnumerable<string> keys2 = mine.Keys;
				second = keys2;
			}
			else
			{
				second = Enumerable.Empty<string>();
			}
			foreach (string item in from g in (from g in keys.Concat(second)
					where g.StartsWith("com.helvetica.") && g != "com.helvetica.modnet"
					select g).Distinct()
				orderby DisplayName(g)
				select g)
			{
				string value;
				bool num = p.Mods.TryGetValue(item, out value);
				mine.TryGetValue(item, out var value2);
				if (!num)
				{
					list.Add("<color=#E0A040>no " + DisplayName(item) + "</color>");
				}
				else if (!p.IsMe && value2 != null && value2 != value)
				{
					list.Add("<color=#E0A040>" + DisplayName(item) + " " + value + " (you: " + value2 + ")</color>");
				}
				else
				{
					list.Add(DisplayName(item) + " " + value);
				}
			}
			string value3;
			string text2 = (p.Mods.TryGetValue("com.helvetica.modnet", out value3) ? value3 : "?");
			b.AddLabel(text + "  <color=#9A8F85>ModNet " + text2 + "</color>\n  " + ((list.Count > 0) ? string.Join(", ", list) : "no other mods"));
		}

		private static string DisplayName(string guid)
		{
			if (!Chainloader.PluginInfos.TryGetValue(guid, out var value))
			{
				return Session.Short(guid);
			}
			return value.Metadata.Name;
		}

		private static void Notify(string message)
		{
			Plugin.ModLog.LogInfo((object)message);
			NotificationManager instance = NotificationManager.instance;
			if ((Object)(object)instance != (Object)null)
			{
				instance.DisplayNotification(message);
			}
		}
	}
	public static class ModNetApi
	{
		public const string Guid = "com.helvetica.modnet";

		public const int Protocol = 1;

		public const int MaxPayload = 1000;

		public static bool Ready => Session.IsReady;

		public static bool IsMultiplayer => Session.IsMultiplayer;

		public static int MyOwnerId => Session.MyOwnerId;

		public static IReadOnlyList<PlayerInfo> Players => Session.Players;

		public static event Action BecameReady;

		public static event Action<PlayerInfo> PlayerHello;

		public static event Action<int> PlayerLeft;

		public static void Register(string channel, Action<NetMessage> handler)
		{
			Transport.Register(channel, handler);
		}

		public static bool Send(string channel, string payload, Character subject = null)
		{
			return Transport.Send(channel, payload, subject);
		}

		public static PlayerInfo GetPlayer(int ownerId)
		{
			return Session.Players.FirstOrDefault((PlayerInfo p) => p.OwnerId == ownerId);
		}

		public static bool EveryoneHas(string pluginGuid, string version = null)
		{
			if (!Session.IsMultiplayer)
			{
				return true;
			}
			string value;
			if (Session.Players.Count > 0)
			{
				return Session.Players.All((PlayerInfo p) => p.Mods.TryGetValue(pluginGuid, out value) && (version == null || value == version));
			}
			return false;
		}

		internal static void RaiseReady()
		{
			Invoke(ModNetApi.BecameReady, delegate(Action h)
			{
				h();
			});
		}

		internal static void RaiseHello(PlayerInfo p)
		{
			Invoke(ModNetApi.PlayerHello, delegate(Action<PlayerInfo> h)
			{
				h(p);
			});
		}

		internal static void RaiseLeft(int ownerId)
		{
			Invoke(ModNetApi.PlayerLeft, delegate(Action<int> h)
			{
				h(ownerId);
			});
		}

		private static void Invoke<T>(T evt, Action<T> call) where T : Delegate
		{
			if (evt == null)
			{
				return;
			}
			Delegate[] invocationList = evt.GetInvocationList();
			foreach (Delegate obj in invocationList)
			{
				try
				{
					call((T)obj);
				}
				catch (Exception arg)
				{
					Plugin.ModLog.LogError((object)$"ModNet event handler {obj.Method.DeclaringType?.Name}.{obj.Method.Name} failed: {arg}");
				}
			}
		}
	}
	public sealed class NetMessage
	{
		public string Channel { get; internal set; }

		public string Payload { get; internal set; }

		public Character Character { get; internal set; }

		public int SenderOwnerId { get; internal set; }

		public PlayerInfo Sender => ModNetApi.GetPlayer(SenderOwnerId);
	}
	public sealed class PlayerInfo
	{
		internal Dictionary<string, string> ModsInternal = new Dictionary<string, string>();

		public int OwnerId { get; internal set; }

		public string Name { get; internal set; }

		public bool IsMe { get; internal set; }

		public bool HasModNet { get; internal set; }

		public ulong SteamId { get; internal set; }

		public IReadOnlyDictionary<string, string> Mods => ModsInternal;

		public bool Has(string pluginGuid, string version = null)
		{
			if (ModsInternal.TryGetValue(pluginGuid, out var value))
			{
				if (version != null)
				{
					return value == version;
				}
				return true;
			}
			return false;
		}
	}
	[BepInPlugin("com.helvetica.modnet", "ModNet", "0.1.3")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : StolenRealmMod
	{
		public const string PluginGuid = "com.helvetica.modnet";

		public const string PluginName = "ModNet";

		public const string PluginVersion = "0.1.3";

		internal static ManualLogSource ModLog;

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> LogTraffic;

		internal static ConfigEntry<bool> TestTools;

		protected override void OnModLoaded()
		{
			ModLog = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Exchange messages with other modded players in multiplayer games (shared emotes, mod list, exact Steam nicknames...). Off: our mods behave as if nobody else had them.");
			LogTraffic = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "LogTraffic", false, "Log every message sent and received (turn on when reporting a multiplayer problem).");
			TestTools = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "TestTools", false, "Show the Ping and Payload size test buttons on this card.");
			Session.Install();
			if (Chainloader.PluginInfos.ContainsKey("com.helvetica.modsmenu"))
			{
				MenuCard.Register();
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} v{1} loaded (protocol {2}).", "ModNet", "0.1.3", 1));
		}
	}
	internal class Session : MonoBehaviour
	{
		private sealed class Tracked
		{
			public PlayerInfo Info;

			public bool Connected;

			public bool HelloRaised;
		}

		internal const string Channel = "modnet";

		private const float SettleSeconds = 3f;

		private static readonly List<PlayerInfo> PlayerList = new List<PlayerInfo>();

		private static readonly Dictionary<int, Tracked> ById = new Dictionary<int, Tracked>();

		private static readonly Random Rng = new Random();

		private int _generation = int.MinValue;

		private float _readySince = -1f;

		private float _nextSync;

		private float _replyAt = -1f;

		private string _lastError;

		private static readonly Dictionary<int, float> Pings = new Dictionary<int, float>();

		private static int _nextPing;

		private static Character _sender;

		private static Session Instance;

		internal static bool IsMultiplayer { get; private set; }

		internal static bool IsReady { get; private set; }

		internal static int MyOwnerId { get; private set; } = -1;

		internal static IReadOnlyList<PlayerInfo> Players => PlayerList;

		internal static Root Root
		{
			get
			{
				NetworkingManager instance = NetworkingManager.Instance;
				if (!((Object)(object)instance != (Object)null) || instance.NetworkManager == null)
				{
					return null;
				}
				return ((Observer<Root>)(object)instance.NetworkManager).Root;
			}
		}

		internal static void Install()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			GameObject val = new GameObject("ModNet");
			Object.DontDestroyOnLoad((Object)val);
			((Object)val).hideFlags = (HideFlags)61;
			val.AddComponent<Session>();
			Transport.Register("modnet", OnMessage);
		}

		private void Update()
		{
			try
			{
				Tick();
				Transport.Flush();
			}
			catch (Exception ex)
			{
				if (ex.ToString() != _lastError)
				{
					_lastError = ex.ToString();
					Plugin.ModLog.LogError((object)ex);
				}
			}
		}

		private void Tick()
		{
			NetworkingManager instance = NetworkingManager.Instance;
			int num = (((Object)(object)instance != (Object)null) ? instance.SessionGeneration : int.MinValue);
			bool flag = (Object)(object)instance != (Object)null && !instance.IsPlayingAsSinglePlayer && Root != null;
			if (num != _generation || flag != IsMultiplayer)
			{
				_generation = num;
				IsMultiplayer = flag;
				Reset();
			}
			if (!flag)
			{
				return;
			}
			if (Time.unscaledTime >= _nextSync)
			{
				_nextSync = Time.unscaledTime + 1f;
				SyncPlayers();
			}
			Character val = (_sender = FindSender());
			if (val == null || !Plugin.Enabled.Value)
			{
				if (IsReady)
				{
					Plugin.ModLog.LogInfo((object)"ModNet: paused (no characters in the world, or turned off).");
				}
				IsReady = false;
				_readySince = -1f;
				return;
			}
			if (_readySince < 0f)
			{
				_readySince = Time.unscaledTime;
			}
			if (!IsReady && Time.unscaledTime - _readySince >= 3f)
			{
				IsReady = true;
				MyOwnerId = val.OwnerID;
				SyncPlayers();
				Plugin.ModLog.LogInfo((object)(string.Format("ModNet ready: I am #{0}, {1}, ", MyOwnerId, instance.IsServer ? "host" : "client") + $"{PlayerList.Count} player(s). {LobbyInfo()}"));
				SendHello(request: true);
				ModNetApi.RaiseReady();
			}
			if (IsReady && _replyAt >= 0f && Time.unscaledTime >= _replyAt)
			{
				_replyAt = -1f;
				SendHello(request: false);
			}
		}

		private static void Reset()
		{
			_sender = null;
			IsReady = false;
			MyOwnerId = -1;
			ById.Clear();
			PlayerList.Clear();
			Pings.Clear();
		}

		internal static Character SenderCharacter()
		{
			return _sender;
		}

		private static Character FindSender()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Invalid comparison between Unknown and I4
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Invalid comparison between Unknown and I4
			NetworkingManager instance = NetworkingManager.Instance;
			GUIManager instance2 = GUIManager.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null)
			{
				return null;
			}
			GUIState currentGuiState = instance2.CurrentGuiState;
			if ((int)currentGuiState == 5 || (int)currentGuiState == 0 || (int)currentGuiState == 1)
			{
				return null;
			}
			List<Character> myCharacters = instance.MyCharacters;
			List<Character> allPlayerCharacters = instance.AllPlayerCharacters;
			if (myCharacters == null || allPlayerCharacters == null)
			{
				return null;
			}
			foreach (Character item in myCharacters)
			{
				if (item != null && (Object)(object)item.PlayerMovement != (Object)null && allPlayerCharacters.Contains(item))
				{
					return item;
				}
			}
			return null;
		}

		internal static bool IsMine(Character c)
		{
			try
			{
				return c != null && c.Owned;
			}
			catch (Exception)
			{
				return false;
			}
		}

		private static void SyncPlayers()
		{
			Root root = Root;
			ObservableDictionary<int, string> val = ((root != null) ? root.ConnectionNameDict : null);
			if (val == null)
			{
				return;
			}
			ObservableDictionary<int, bool> activeConnections = root.ActiveConnections;
			HashSet<int> present = new HashSet<int>();
			bool flag = default(bool);
			string text = default(string);
			foreach (int item in val.Keys.ToList())
			{
				if (activeConnections == null || !activeConnections.TryGetValue(item, ref flag) || flag)
				{
					present.Add(item);
					Tracked tracked = Get(item);
					tracked.Connected = true;
					if (val.TryGetValue(item, ref text) && !string.IsNullOrEmpty(text))
					{
						tracked.Info.Name = text;
					}
				}
			}
			foreach (Tracked item2 in ById.Values.Where((Tracked t) => t.Connected && !present.Contains(t.Info.OwnerId)).ToList())
			{
				ById.Remove(item2.Info.OwnerId);
				Plugin.ModLog.LogInfo((object)$"ModNet: {item2.Info.Name} (#{item2.Info.OwnerId}) left.");
				ModNetApi.RaiseLeft(item2.Info.OwnerId);
			}
			if (MyOwnerId >= 0)
			{
				Tracked tracked2 = Get(MyOwnerId);
				PlayerInfo info = tracked2.Info;
				bool isMe = (tracked2.Info.HasModNet = true);
				info.IsMe = isMe;
				tracked2.Info.SteamId = MySteamId();
				tracked2.Info.ModsInternal = LocalMods();
			}
			PlayerList.Clear();
			PlayerList.AddRange(from t in ById.Values
				where t.Connected || t.Info.HasModNet
				select t.Info into p
				orderby p.OwnerId
				select p);
		}

		private static Tracked Get(int id)
		{
			if (!ById.TryGetValue(id, out var value))
			{
				Dictionary<int, Tracked> byId = ById;
				int key = id;
				Tracked obj = new Tracked
				{
					Info = new PlayerInfo
					{
						OwnerId = id,
						Name = "Player #" + id
					}
				};
				value = obj;
				byId[key] = obj;
			}
			return value;
		}

		private static void SendHello(bool request)
		{
			IEnumerable<string> enumerable = from kv in LocalMods()
				orderby (!kv.Key.StartsWith("com.helvetica.")) ? 1 : 0, kv.Key
				select kv.Key + "=" + kv.Value;
			string text = $"h|{(request ? 1 : 0)}|{MySteamId()}|";
			List<string> list = new List<string>();
			int num = text.Length;
			foreach (string item in enumerable)
			{
				if (num + item.Length + 1 > 1000)
				{
					break;
				}
				list.Add(item);
				num += item.Length + 1;
			}
			Transport.Send("modnet", text + string.Join(",", list), null, unlimited: true);
		}

		private void ScheduleReply()
		{
			if (_replyAt < 0f)
			{
				_replyAt = Time.unscaledTime + 0.2f + (float)Rng.NextDouble() * 0.8f;
			}
		}

		private static void OnHello(NetMessage m, string[] f)
		{
			if (f.Length < 4)
			{
				return;
			}
			Tracked tracked = Get(m.SenderOwnerId);
			tracked.Info.HasModNet = true;
			ulong.TryParse(f[2], out var result);
			tracked.Info.SteamId = result;
			Dictionary<string, string> dictionary = new Dictionary<string, string>();
			string[] array = f[3].Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string text in array)
			{
				int num = text.IndexOf('=');
				if (num > 0)
				{
					dictionary[text.Substring(0, num)] = text.Substring(num + 1);
				}
			}
			tracked.Info.ModsInternal = dictionary;
			SyncPlayers();
			if (f[1] == "1")
			{
				Instance?.ScheduleReply();
			}
			if (!tracked.HelloRaised)
			{
				tracked.HelloRaised = true;
				Plugin.ModLog.LogInfo((object)($"ModNet: hello from {tracked.Info.Name} (#{tracked.Info.OwnerId}): " + string.Join(", ", from kv in dictionary
					where kv.Key.StartsWith("com.helvetica.")
					select Short(kv.Key) + " " + kv.Value)));
				ModNetApi.RaiseHello(tracked.Info);
			}
		}

		private void Awake()
		{
			Instance = this;
		}

		internal static Dictionary<string, string> LocalMods()
		{
			return (from i in Chainloader.PluginInfos.Values
				where ((i != null) ? i.Metadata : null) != null
				group i by i.Metadata.GUID).ToDictionary((IGrouping<string, PluginInfo> g) => g.Key, (IGrouping<string, PluginInfo> g) => g.First().Metadata.Version.ToString());
		}

		internal static string Short(string guid)
		{
			if (!guid.StartsWith("com.helvetica."))
			{
				return guid;
			}
			return guid.Substring("com.helvetica.".Length);
		}

		private static ulong MySteamId()
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				return SteamClient.IsValid ? SteamClient.SteamId.Value : 0;
			}
			catch (Exception)
			{
				return 0uL;
			}
		}

		internal static string LobbyInfo()
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				SteamManager instance = SteamManager.instance;
				Lobby? val = (((Object)(object)instance != (Object)null) ? instance.CurrentLobby : ((Lobby?)null));
				object result;
				if (!val.HasValue)
				{
					result = "Steam lobby: none.";
				}
				else
				{
					Lobby value = val.Value;
					result = $"Steam lobby: {((Lobby)(ref value)).MemberCount} member(s).";
				}
				return (string)result;
			}
			catch (Exception ex)
			{
				return "Steam lobby: unknown (" + ex.GetType().Name + ").";
			}
		}

		private static void OnMessage(NetMessage m)
		{
			string[] array = m.Payload.Split('|');
			switch (array[0])
			{
			case "h":
				OnHello(m, array);
				break;
			case "p":
				Transport.Send("modnet", $"q|{array[1]}|{m.SenderOwnerId}", null);
				break;
			case "q":
			{
				if (array.Length >= 3 && array[2] == MyOwnerId.ToString() && int.TryParse(array[1], out var result) && Pings.TryGetValue(result, out var value))
				{
					Plugin.ModLog.LogInfo((object)$"ModNet ping: {Name(m)} answered in {(Time.unscaledTime - value) * 1000f:0} ms (round trip).");
				}
				break;
			}
			case "s":
			{
				if (array.Length >= 3 && int.TryParse(array[1], out var result2))
				{
					bool flag = array[2].Length == result2 && array[2].All((char ch) => ch == 'x');
					Plugin.ModLog.LogInfo((object)string.Format("ModNet size test: {0} chars from {1} arrived {2}.", result2, Name(m), flag ? "intact" : $"DAMAGED ({array[2].Length} chars)"));
					Transport.Send("modnet", $"t|{result2}|{m.SenderOwnerId}|{(flag ? 1 : 0)}", null);
				}
				break;
			}
			case "t":
				if (array.Length >= 4 && array[2] == MyOwnerId.ToString())
				{
					Plugin.ModLog.LogInfo((object)("ModNet size test: " + Name(m) + " got " + array[1] + " chars " + ((array[3] == "1") ? "intact" : "DAMAGED") + "."));
				}
				break;
			}
		}

		private static string Name(NetMessage m)
		{
			return m.Sender?.Name ?? ("#" + m.SenderOwnerId);
		}

		internal static string Ping()
		{
			if (!IsReady)
			{
				return "Not ready: join a multiplayer game first.";
			}
			int key = ++_nextPing;
			Pings[key] = Time.unscaledTime;
			if (!Transport.Send("modnet", "p|" + key, null))
			{
				return "Ping not sent (see the log).";
			}
			return "Ping sent; answers go to the BepInEx log.";
		}

		internal static string SizeTest()
		{
			if (!IsReady)
			{
				return "Not ready: join a multiplayer game first.";
			}
			int[] array = new int[5] { 250, 500, 1000, 2000, 4000 };
			foreach (int num in array)
			{
				bool flag = Transport.Send("modnet", $"s|{num}|{new string('x', num)}", null, unlimited: true);
				Plugin.ModLog.LogInfo((object)string.Format("ModNet size test: sent {0} chars: {1}.", num, flag ? "ok" : "FAILED locally"));
				if (!flag)
				{
					break;
				}
			}
			return "Size test sent (250 to 4000 chars); results go to the BepInEx log on every PC.";
		}
	}
	internal static class Transport
	{
		private struct Pending
		{
			public string Channel;

			public string Payload;

			public Character Subject;

			public bool Unlimited;
		}

		internal const string Prefix = "~srm";

		internal static readonly string Marker = "~srm" + 1 + "|";

		private const float PerSecond = 10f;

		private const float BurstSize = 20f;

		private static float _tokens = 20f;

		private static float _lastRefill;

		private static readonly Dictionary<string, List<Action<NetMessage>>> Handlers = new Dictionary<string, List<Action<NetMessage>>>(StringComparer.Ordinal);

		private static readonly HashSet<string> Warned = new HashSet<string>();

		private static bool _sending;

		private static int _receiving;

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

		internal static int Sent;

		internal static int Received;

		internal static void Register(string channel, Action<NetMessage> handler)
		{
			if (!string.IsNullOrEmpty(channel) && handler != null)
			{
				if (channel.IndexOf('|') >= 0)
				{
					throw new ArgumentException("Channel names can't contain '|'.", "channel");
				}
				if (!Handlers.TryGetValue(channel, out var value))
				{
					value = (Handlers[channel] = new List<Action<NetMessage>>());
				}
				value.Add(handler);
			}
		}

		internal static bool Send(string channel, string payload, Character subject, bool unlimited = false)
		{
			if (!Plugin.Enabled.Value || !Session.IsReady)
			{
				return false;
			}
			if (string.IsNullOrEmpty(channel) || channel.IndexOf('|') >= 0)
			{
				WarnOnce("bad channel name: " + channel);
				return false;
			}
			if (payload == null)
			{
				payload = "";
			}
			if (!unlimited && payload.Length > 1000)
			{
				WarnOnce($"{channel}: payload of {payload.Length} chars is over the {1000} limit; not sent.");
				return false;
			}
			if (_receiving > 0)
			{
				Deferred.Enqueue(new Pending
				{
					Channel = channel,
					Payload = payload,
					Subject = subject,
					Unlimited = unlimited
				});
				return true;
			}
			return SendNow(channel, payload, subject, unlimited, queued: false);
		}

		internal static void Flush()
		{
			if (Deferred.Count == 0)
			{
				return;
			}
			if (!Plugin.Enabled.Value || !Session.IsReady)
			{
				Deferred.Clear();
				return;
			}
			while (Deferred.Count > 0)
			{
				Pending pending = Deferred.Dequeue();
				if (pending.Subject == null || (pending.Subject != null && Session.IsMine(pending.Subject)))
				{
					SendNow(pending.Channel, pending.Payload, pending.Subject, pending.Unlimited, queued: true);
				}
			}
		}

		private static bool SendNow(string channel, string payload, Character subject, bool unlimited, bool queued)
		{
			if (!unlimited && !TakeToken())
			{
				WarnOnce($"{channel}: sending too fast; some messages were dropped (limit {10f}/s).");
				return false;
			}
			Character val = subject ?? Session.SenderCharacter();
			if (val == null || !Session.IsMine(val))
			{
				WarnOnce(channel + ": the subject must be one of your characters.");
				return false;
			}
			try
			{
				if ((Object)(object)val.PlayerMovement == (Object)null)
				{
					return false;
				}
			}
			catch (Exception)
			{
				return false;
			}
			Root root = Session.Root;
			if (root == null)
			{
				return false;
			}
			string text = Marker + channel + "|" + payload;
			_sending = true;
			try
			{
				root.SetAnimationTrigger(val, text);
			}
			catch (Exception arg)
			{
				Plugin.ModLog.LogError((object)$"ModNet: sending on '{channel}' failed: {arg}");
				return false;
			}
			finally
			{
				_sending = false;
			}
			Sent++;
			if (Plugin.LogTraffic.Value)
			{
				Plugin.ModLog.LogInfo((object)string.Format("ModNet → {0} ({1} chars{2}): {3}", channel, payload.Length, queued ? ", queued during a receive" : "", Preview(payload)));
			}
			return true;
		}

		internal static void Receive(Character character, string wire)
		{
			if (_sending)
			{
				return;
			}
			if (!wire.StartsWith(Marker, StringComparison.Ordinal))
			{
				WarnOnce("ignoring a message from another ModNet protocol version: " + Preview(wire));
				return;
			}
			int num = wire.IndexOf('|', Marker.Length);
			if (num < 0)
			{
				return;
			}
			string text = wire.Substring(Marker.Length, num - Marker.Length);
			string text2 = wire.Substring(num + 1);
			if (character == null)
			{
				WarnOnce(text + ": message arrived without a character; ignored.");
				return;
			}
			int ownerID;
			try
			{
				ownerID = character.OwnerID;
			}
			catch (Exception)
			{
				return;
			}
			if (ownerID == Session.MyOwnerId)
			{
				return;
			}
			Received++;
			if (Plugin.LogTraffic.Value)
			{
				Plugin.ModLog.LogInfo((object)$"ModNet ← {text} from #{ownerID} ({text2.Length} chars): {Preview(text2)}");
			}
			if (!Plugin.Enabled.Value || !Handlers.TryGetValue(text, out var value))
			{
				return;
			}
			NetMessage obj = new NetMessage
			{
				Channel = text,
				Payload = text2,
				Character = character,
				SenderOwnerId = ownerID
			};
			_receiving++;
			try
			{
				Action<NetMessage>[] array = value.ToArray();
				foreach (Action<NetMessage> action in array)
				{
					try
					{
						action(obj);
					}
					catch (Exception arg)
					{
						Plugin.ModLog.LogError((object)$"ModNet handler for '{text}' failed: {arg}");
					}
				}
			}
			finally
			{
				_receiving--;
			}
		}

		private static bool TakeToken()
		{
			float unscaledTime = Time.unscaledTime;
			_tokens = Mathf.Min(20f, _tokens + (unscaledTime - _lastRefill) * 10f);
			_lastRefill = unscaledTime;
			if (_tokens < 1f)
			{
				return false;
			}
			_tokens -= 1f;
			return true;
		}

		private static string Preview(string s)
		{
			if (s.Length > 80)
			{
				return s.Substring(0, 77) + "...";
			}
			return s;
		}

		internal static void WarnOnce(string message)
		{
			if (Warned.Add(message))
			{
				Plugin.ModLog.LogWarning((object)("ModNet: " + message));
			}
		}
	}
	[HarmonyPatch(typeof(Root), "SetAnimationTrigger")]
	internal static class CarrierPatch
	{
		private static bool Prefix(Character character, string triggerName)
		{
			if (triggerName == null || !triggerName.StartsWith("~srm", StringComparison.Ordinal))
			{
				return true;
			}
			try
			{
				Transport.Receive(character, triggerName);
			}
			catch (Exception arg)
			{
				Plugin.ModLog.LogError((object)$"ModNet: receive failed: {arg}");
			}
			return false;
		}
	}
}