Decompiled source of TooLate v0.1.1

plugins/TooLate.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Unity.IL2CPP;
using CatLib.Config;
using CatLib.Core;
using CatLib.DevTools;
using CatLib.Game.Events;
using CatLib.Localization;
using CatLib.Logging;
using CatLib.Net;
using CatLib.Patching;
using CatLib.Threading;
using CatLib.UI;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppMicrosoft.Data.Sqlite;
using Il2CppSystem;
using Il2CppSystem.Collections.Generic;
using Il2CppSystem.Data.Common;
using Il2CppSystem.Reflection;
using Microsoft.CodeAnalysis;
using TMPro;
using TooLate.Logic;
using TooLate.Patches;
using TooLate.Research;
using TooLate.Scene;
using TooLate.Settings;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("CatLib contributors")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+1b73f398a8bbe07fec967f69ef1e969788e687bd")]
[assembly: AssemblyProduct("CatLib")]
[assembly: AssemblyTitle("TooLate")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsUnmanagedAttribute : Attribute
	{
	}
	[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 TooLate
{
	[BepInPlugin("catlib.toolate", "Too Late", "0.1.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class TooLatePlugin : BasePlugin
	{
		public const string LanguageResourcePrefix = "TooLate.Lang.";

		public const string DevGroup = "Too Late";

		public static JoinCoordinator Joins { get; private set; }

		public override void Load()
		{
			CatLogger val = CatLogger.From(((BasePlugin)this).Log);
			CatSettings val2 = CatSettings.For((BasePlugin)(object)this);
			int count = val2.Texts.Count;
			CatNetwork.Declare((BasePlugin)(object)this, (SessionPolicy)2, (VersionRule)2);
			JoinCoordinator joins = new JoinCoordinator(val, val2.Texts);
			joins.Settings = new JoinSettings(val2, null);
			Joins = joins;
			MethodInfo methodInfo = typeof(Server).GetMethods().FirstOrDefault((MethodInfo method) => method.Name == "HandleClientConnect" && method.GetParameters().Length == 2 && method.GetParameters()[0].ParameterType == typeof(ulong));
			CodePatch loading = CodePatch.Locate("catlib.toolate", "joining a level", val, (MethodBase)methodInfo, "44 38 A1 90 00 00 00 0F 85 ?? ?? ?? ??", 7, 6, 2048);
			CodePatch limit = CodePatch.Locate("catlib.toolate", "player limit", val, (MethodBase)methodInfo, "E8 ?? ?? ?? ?? 83 F8 05 0F 8D", 7, 1, 2048);
			joins.UseCodePatches(loading, limit);
			LobbyOverflow lobby = new LobbyOverflow(val, val2.Texts);
			MessageRecorder recorder = new MessageRecorder(val);
			CatPatches patches = TooLatePatches.Install("catlib.toolate", joins, lobby, recorder, val);
			patches.TurnedOff += delegate
			{
				MainThread.Post((Action)joins.ShutDown);
			};
			FrameLoop.Update += delegate
			{
				patches.Run("Update", (Action)joins.Update);
				patches.Run("Recorder", (Action)recorder.Update);
			};
			NetworkEvents.ClientConnected += delegate(ulong id)
			{
				patches.Run("ClientConnected", (Action)delegate
				{
					joins.OnClientConnected(id);
				});
			};
			NetworkEvents.ClientDisconnected += delegate(ulong id)
			{
				patches.Run("ClientDisconnected", (Action)delegate
				{
					joins.OnClientDisconnected(id);
				});
			};
			BootstrapEvents.GameStartedFromLobby += delegate
			{
				patches.Run("GameStarted", (Action)joins.EnterLevel);
			};
			BootstrapEvents.LevelLoadStarted += delegate
			{
				patches.Run("LevelLoadStarted", (Action)joins.EnterLevel);
			};
			BootstrapEvents.GameRestartStarted += delegate
			{
				joins.LeaveLevel(disconnectWaiting: true);
			};
			BootstrapEvents.MainMenuLoaded += delegate
			{
				joins.LeaveLevel(disconnectWaiting: false);
				lobby.Forget();
			};
			DevMenu.Toggle("Too Late", "Record game messages", (Func<bool>)(() => recorder.IsEnabled), (Action<bool>)recorder.SetEnabled, "Writes every network message of the game to the log: what the host sends to each player and what this game sends to the host.");
			DevMenu.Command("Too Late", "Joining players in the log", (Func<string>)joins.Describe, "Writes whether players can join your level now, the player limit and every player who is joining.");
			DevMenu.Command("Too Late", "Snapshot in the log", (Func<string>)joins.SnapshotProbe, "Writes the warehouse as it is now into a separate file in BepInEx/cache/TooLate, like for a joining player, and writes what it holds.");
			val.Info($"Too Late {"0.1.1"} loaded with {count} translated text(s) in {string.Join(", ", val2.Texts.Languages)}, " + ((patches.IsActive && joins.CanJoinInLevel) ? "players can join your levels" : "joining a level stays closed like in the game") + (joins.CanRaiseLimit ? string.Empty : ", the player limit stays 4"));
		}
	}
	public static class PluginMeta
	{
		public const string Guid = "catlib.toolate";

		public const string Name = "Too Late";

		public const string Version = "0.1.1";
	}
}
namespace TooLate.Settings
{
	public sealed class JoinSettings
	{
		public Setting<bool> Enabled { get; }

		public Setting<int> MaxPlayers { get; }

		public Setting<bool> Messages { get; }

		public JoinSettings(CatSettings settings, Action changed)
		{
			Enabled = settings.Local<bool>("Join", "Enabled", true, "Players can join while you are already playing. They get the warehouse as it is right now. Only the host needs the mod.", (AcceptableValueBase)null);
			MaxPlayers = settings.Local<int>("Join", "MaxPlayers", 4, "How many players fit in your game, you included. 4 is the game's limit. Above 4 the lobby shows the extra players as a count, and the counters stay four.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(4, 8));
			Messages = settings.Local<bool>("Messages", "Enabled", true, "Tells you when a player starts joining, waits for the end of the day's results and is in the game. Only for you.", (AcceptableValueBase)null);
			Enabled.Changed += delegate
			{
				changed?.Invoke();
			};
			MaxPlayers.Changed += delegate
			{
				changed?.Invoke();
			};
		}
	}
}
namespace TooLate.Scene
{
	public static class GameProtocol
	{
		[ThreadStatic]
		private static bool _sending;

		public static bool IsSending => _sending;

		public static NetworkManager Network
		{
			get
			{
				if (!Singleton<NetworkManager>.HasInstance())
				{
					return null;
				}
				return Singleton<NetworkManager>.Instance;
			}
		}

		public static Server Server
		{
			get
			{
				NetworkManager network = Network;
				if (!((Object)(object)network == (Object)null) && network.IsServer)
				{
					return network._server;
				}
				return null;
			}
		}

		public static bool IsHosting => Server != null;

		public static Il2CppReferenceArray<Object> Empty()
		{
			return new Il2CppReferenceArray<Object>(0L);
		}

		public static bool Send(int code, ulong target, Il2CppReferenceArray<Object> payload, bool reliable = true)
		{
			Server server = Server;
			if (server == null)
			{
				return false;
			}
			bool sending = _sending;
			_sending = true;
			try
			{
				server.SendMessage((ProtocolCode)code, reliable, target, payload ?? Empty());
				return true;
			}
			finally
			{
				_sending = sending;
			}
		}

		public static void SendSave(ulong target, string saveName, byte[] data)
		{
			NetworkManager val = Network ?? throw new InvalidOperationException("The game has no network manager");
			bool sending = _sending;
			_sending = true;
			try
			{
				val.SendSaveSynchronization(target, false, false, false, saveName, Il2CppStructArray<byte>.op_Implicit(data));
			}
			finally
			{
				_sending = sending;
			}
		}

		public static void Disconnect(ulong target)
		{
			Server server = Server;
			if (server != null)
			{
				server.DisconnectClient(target);
			}
		}

		public static Il2CppReferenceArray<Object> Copy(Il2CppReferenceArray<Object> payload)
		{
			if (payload == null)
			{
				return Empty();
			}
			Il2CppReferenceArray<Object> val = new Il2CppReferenceArray<Object>((long)((Il2CppArrayBase<Object>)(object)payload).Length);
			for (int i = 0; i < ((Il2CppArrayBase<Object>)(object)payload).Length; i++)
			{
				((Il2CppArrayBase<Object>)(object)val)[i] = ((Il2CppArrayBase<Object>)(object)payload)[i];
			}
			return val;
		}

		public static Il2CppReferenceArray<Object> Identifiers(IReadOnlyList<EntityPlace> places, uint nextIdentifier)
		{
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			Il2CppReferenceArray<Object> val = new Il2CppReferenceArray<Object>((long)(places.Count * 2 + 2));
			int num = 0;
			foreach (EntityPlace place in places)
			{
				((Il2CppArrayBase<Object>)(object)val)[num++] = Box(place.Id);
				((Il2CppArrayBase<Object>)(object)val)[num++] = Box(new Vector3(place.X, place.Y, place.Z));
			}
			((Il2CppArrayBase<Object>)(object)val)[num++] = Box(uint.MaxValue);
			((Il2CppArrayBase<Object>)(object)val)[num] = Box(nextIdentifier);
			return val;
		}

		public static Il2CppReferenceArray<Object> Generic(uint entityId, string text)
		{
			//IL_0025: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Expected O, but got Unknown
			return new Il2CppReferenceArray<Object>(2L)
			{
				[0] = Box(entityId),
				[1] = new Object(IL2CPP.ManagedStringToIl2Cpp(text ?? string.Empty))
			};
		}

		public static bool TryReadGeneric(Il2CppReferenceArray<Object> payload, out uint entityId, out string text)
		{
			text = null;
			if (TryReadId(payload, out entityId) && ((Il2CppArrayBase<Object>)(object)payload).Length >= 2 && ((Il2CppArrayBase<Object>)(object)payload)[1] != null)
			{
				Type il2CppType = ((Il2CppArrayBase<Object>)(object)payload)[1].GetIl2CppType();
				if (!(((il2CppType != null) ? ((MemberInfo)il2CppType).Name : null) != "String"))
				{
					text = IL2CPP.Il2CppStringToManaged(((Il2CppObjectBase)((Il2CppArrayBase<Object>)(object)payload)[1]).Pointer);
					return text != null;
				}
			}
			return false;
		}

		public static bool TryReadId(Il2CppReferenceArray<Object> payload, out uint entityId)
		{
			entityId = 0u;
			if (payload != null && ((Il2CppArrayBase<Object>)(object)payload).Length >= 1 && ((Il2CppArrayBase<Object>)(object)payload)[0] != null)
			{
				Type il2CppType = ((Il2CppArrayBase<Object>)(object)payload)[0].GetIl2CppType();
				if (!(((il2CppType != null) ? ((MemberInfo)il2CppType).Name : null) != "UInt32"))
				{
					entityId = ((Il2CppObjectBase)((Il2CppArrayBase<Object>)(object)payload)[0]).Unbox<uint>();
					return true;
				}
			}
			return false;
		}

		public static Il2CppReferenceArray<Object> TimePeriod(int period)
		{
			return new Il2CppReferenceArray<Object>(1L) { [0] = Box((byte)period, ClassOf<byte, Byte>()) };
		}

		public static Object Box(uint value)
		{
			return Box(value, ClassOf<uint, UInt32>());
		}

		public static Object Box(Vector3 value)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			return Box<Vector3>(value, ClassOf<Vector3, Vector3>());
		}

		private unsafe static Object Box<T>(T value, IntPtr type) where T : unmanaged
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Expected O, but got Unknown
			if (type == IntPtr.Zero)
			{
				throw new InvalidOperationException("The game has no class for " + typeof(T).Name);
			}
			return new Object(IL2CPP.il2cpp_value_box(type, (IntPtr)(&value)));
		}

		private static IntPtr ClassOf<TManaged, TGame>()
		{
			RuntimeHelpers.RunClassConstructor(typeof(TGame).TypeHandle);
			IntPtr nativeClassPtr = Il2CppClassPointerStore<TManaged>.NativeClassPtr;
			if (!(nativeClassPtr != IntPtr.Zero))
			{
				return Il2CppClassPointerStore<TGame>.NativeClassPtr;
			}
			return nativeClassPtr;
		}

		public static string Describe(Il2CppReferenceArray<Object> payload, int most = 6)
		{
			if (payload == null || ((Il2CppArrayBase<Object>)(object)payload).Length == 0)
			{
				return "no values";
			}
			List<string> list = new List<string>();
			for (int i = 0; i < ((Il2CppArrayBase<Object>)(object)payload).Length && i < most; i++)
			{
				list.Add(DescribeValue(((Il2CppArrayBase<Object>)(object)payload)[i]));
			}
			if (((Il2CppArrayBase<Object>)(object)payload).Length > most)
			{
				list.Add($"... {((Il2CppArrayBase<Object>)(object)payload).Length - most} more");
			}
			return $"{((Il2CppArrayBase<Object>)(object)payload).Length} value(s): {string.Join(", ", list)}";
		}

		private static string DescribeValue(Object value)
		{
			//IL_0212: Unknown result type (might be due to invalid IL or missing references)
			//IL_0217: Unknown result type (might be due to invalid IL or missing references)
			//IL_022f: Unknown result type (might be due to invalid IL or missing references)
			//IL_024d: Unknown result type (might be due to invalid IL or missing references)
			//IL_026b: Unknown result type (might be due to invalid IL or missing references)
			if (value == null)
			{
				return "null";
			}
			try
			{
				Type il2CppType = value.GetIl2CppType();
				string text = ((il2CppType == (Type)null) ? "?" : ((MemberInfo)il2CppType).Name);
				switch (text)
				{
				case "String":
				{
					string text2 = IL2CPP.Il2CppStringToManaged(((Il2CppObjectBase)value).Pointer) ?? string.Empty;
					return (text2.Length > 40) ? ("\"" + text2.Substring(0, 40) + "...\"") : ("\"" + text2 + "\"");
				}
				case "Boolean":
					return ((Il2CppObjectBase)value).Unbox<bool>() ? "true" : "false";
				case "Int32":
					return ((Il2CppObjectBase)value).Unbox<int>().ToString();
				case "UInt32":
					return ((Il2CppObjectBase)value).Unbox<uint>().ToString();
				case "UInt64":
					return ((Il2CppObjectBase)value).Unbox<ulong>().ToString();
				case "Single":
					return ((Il2CppObjectBase)value).Unbox<float>().ToString("0.###");
				case "Vector3":
				{
					Vector3 val = ((Il2CppObjectBase)value).Unbox<Vector3>();
					return $"({val.x:0.##}, {val.y:0.##}, {val.z:0.##})";
				}
				case "Byte[]":
					return $"{((Il2CppArrayBase<byte>)(object)new Il2CppStructArray<byte>(((Il2CppObjectBase)value).Pointer)).Length} bytes";
				default:
					return text;
				}
			}
			catch (Exception ex)
			{
				return "? " + ex.GetType().Name;
			}
		}
	}
	public sealed class JoinCoordinator
	{
		public const double LoadingWarningSeconds = 120.0;

		public const double LoadingGiveUpSeconds = 360.0;

		public const int FlushPerFrame = 200;

		public const double AppearingGraceSeconds = 3.0;

		public const string EventPrefix = "TooLate.";

		private readonly CatLogger _log;

		private readonly TextCatalog _texts;

		private readonly Dictionary<ulong, JoinTicket<Il2CppReferenceArray<Object>>> _tickets = new Dictionary<ulong, JoinTicket<Il2CppReferenceArray<Object>>>();

		private readonly HashSet<ulong> _warned = new HashSet<ulong>();

		private readonly StateLedger _state = new StateLedger();

		private readonly LastingMessages<Il2CppReferenceArray<Object>> _lasting = new LastingMessages<Il2CppReferenceArray<Object>>();

		private CodePatch _loading;

		private CodePatch _limit;

		private bool _loadingOpen;

		private bool _startWritten;

		private bool _inLevel;

		private int _limitWritten = 4;

		public JoinSettings Settings { get; set; }

		public IReadOnlyCollection<JoinTicket<Il2CppReferenceArray<Object>>> Tickets => _tickets.Values;

		public bool CanJoinInLevel
		{
			get
			{
				CodePatch loading = _loading;
				if (loading != null)
				{
					return loading.IsFound;
				}
				return false;
			}
		}

		public bool CanRaiseLimit
		{
			get
			{
				CodePatch limit = _limit;
				if (limit != null)
				{
					return limit.IsFound;
				}
				return false;
			}
		}

		public bool InLevel => _inLevel;

		public JoinCoordinator(CatLogger log, TextCatalog texts)
		{
			_log = log;
			_texts = texts;
		}

		public void UseCodePatches(CodePatch loading, CodePatch limit)
		{
			_loading = loading;
			_limit = limit;
		}

		public void Update()
		{
			HostState state = ReadHost();
			UpdateCodePatches(state);
			if (!state.Hosting)
			{
				if (_tickets.Count > 0)
				{
					_tickets.Clear();
				}
				return;
			}
			foreach (JoinTicket<Il2CppReferenceArray<Object>> item in _tickets.Values.ToList())
			{
				if (item.FailReason != null)
				{
					Fail(item, item.FailReason);
					continue;
				}
				if (item.Flushing)
				{
					Flush(item);
					continue;
				}
				switch (item.Stage)
				{
				case JoinStage.Waiting:
					if (JoinGate.ReadyToStart(state, FrameLoop.Realtime - item.ConnectedAt))
					{
						Start(item);
					}
					else if (!item.WaitingNoticeShown && JoinGate.IsRecap(state.TimePeriod))
					{
						item.WaitingNoticeShown = true;
						Notify("message.waiting", item.Name);
						_log.Info(item.Name + " waits in the lobby until the day's results are over");
					}
					break;
				case JoinStage.Appearing:
					if (IsSpawned(item.ClientId) && FrameLoop.Realtime - item.StageSince > 3.0)
					{
						_log.Warning(item.Name + " appeared without the game sending them the identifiers again, Too Late sends them itself");
						GameProtocol.Send(21, item.ClientId, GameProtocol.Identifiers(item.Places, NextIdentifier()));
						StartFlush(item);
						break;
					}
					goto case JoinStage.Loading;
				case JoinStage.Loading:
					if (item.Overflowed > 0)
					{
						Fail(item, $"more than {20000} game messages piled up while the player was loading");
					}
					else if (FrameLoop.Realtime - item.StageSince > 360.0)
					{
						Fail(item, $"the player did not finish loading in {360.0:0} seconds");
					}
					else if (FrameLoop.Realtime - item.StageSince > 120.0 && _warned.Add(item.ClientId))
					{
						_log.Warning($"{item.Name} has been loading for {120.0:0} seconds, {item.Held.Count} game message(s) wait for them");
					}
					break;
				case JoinStage.Synced:
					if (IsSpawned(item.ClientId))
					{
						item.MoveTo(JoinStage.Playing, FrameLoop.Realtime);
						Notify("message.joined", item.Name);
						Publish("Joined", item);
						_log.Info(item.Name + " is in the game, " + Seconds(FrameLoop.Realtime - item.ConnectedAt) + " after connecting");
					}
					break;
				}
			}
		}

		public void OnClientConnected(ulong clientId)
		{
			if (ShouldTakeOver(clientId))
			{
				Ensure(clientId);
			}
			else if (GameProtocol.IsHosting && CatNetwork.IsRemotePlayer(clientId) && !_tickets.ContainsKey(clientId))
			{
				_log.Info($"Player {clientId} connected and joins the usual way: {(_inLevel ? "joining a game in progress is off in the settings" : "you are in the lobby")}");
			}
		}

		public void OnClientDisconnected(ulong clientId)
		{
			_warned.Remove(clientId);
			if (_tickets.Remove(clientId, out var value))
			{
				Publish("Left", value);
				_log.Info(value.Name + " left while " + value.Stage.ToString().ToLowerInvariant());
			}
		}

		public bool TakesOverSave(ulong clientId)
		{
			if (!ShouldTakeOver(clientId))
			{
				return false;
			}
			Ensure(clientId);
			return true;
		}

		public void Observe(int code, Il2CppReferenceArray<Object> payload)
		{
			bool flag = !_inLevel;
			if (!flag)
			{
				bool flag2 = ((code == 18 || code == 38) ? true : false);
				flag = !flag2 && !LastingMessages<Object>.IsLasting(code);
			}
			if (flag || !GameProtocol.IsHosting)
			{
				return;
			}
			switch (code)
			{
			case 38:
			{
				if (GameProtocol.TryReadGeneric(payload, out var entityId2, out var text))
				{
					_state.Record(entityId2, text);
				}
				return;
			}
			case 18:
			{
				if (GameProtocol.TryReadId(payload, out var entityId))
				{
					_state.Forget(entityId);
					_lasting.Forget(entityId);
				}
				return;
			}
			}
			uint entityId3;
			if (!LastingMessages<Object>.IsPerEntity(code))
			{
				_lasting.Record(code, 0u, GameProtocol.Copy(payload));
			}
			else if (GameProtocol.TryReadId(payload, out entityId3))
			{
				_lasting.Record(code, entityId3, GameProtocol.Copy(payload));
			}
		}

		public bool RouteOutgoing(int code, bool reliable, ulong target, Il2CppReferenceArray<Object> payload)
		{
			if (_tickets.Count == 0 || !_tickets.TryGetValue(target, out var value))
			{
				return true;
			}
			switch (MessageRoute.For(value.Stage, code))
			{
			case Route.Pass:
				return true;
			case Route.Replace:
				try
				{
					SendIdentifiers(value);
				}
				catch (Exception ex)
				{
					_log.Error("Sending the identifiers to " + value.Name + " failed", ex);
					value.FailReason = "the identifiers of the snapshot could not be sent";
				}
				return false;
			case Route.Hold:
			case Route.HoldLatest:
				value.Accept(code, reliable, GameProtocol.Copy(payload));
				return false;
			default:
				value.Accept(code, reliable, null);
				return false;
			}
		}

		public void ShutDown()
		{
			_log.Warning("Too Late turned itself off: players who are still joining are disconnected, joining a level and the player limit are like in the game again");
			foreach (JoinTicket<Il2CppReferenceArray<Object>> item in _tickets.Values.Where(delegate(JoinTicket<Il2CppReferenceArray<Object>> ticket)
			{
				JoinStage stage = ticket.Stage;
				return (uint)stage <= 2u;
			}).ToList())
			{
				GameProtocol.Disconnect(item.ClientId);
			}
			_tickets.Clear();
			_warned.Clear();
			CodePatch loading = _loading;
			if (loading != null && loading.IsFound)
			{
				_loading.Restore();
			}
			loading = _limit;
			if (loading != null && loading.IsFound)
			{
				_limit.Restore();
			}
			_loadingOpen = false;
			_limitWritten = 4;
		}

		public void EnterLevel()
		{
			if (!_inLevel)
			{
				_inLevel = true;
				_startWritten = false;
				ClearState();
				WriteStarted();
			}
		}

		private void WriteStarted()
		{
			if (!_startWritten && _inLevel && GameProtocol.IsHosting)
			{
				_startWritten = true;
				_log.Info(CatNetwork.IsWithoutSteamNetwork ? "Your game started without the Steam network, it runs on this computer only and nobody can join it" : "Your game started, players who connect from now on join the game in progress");
			}
		}

		public void LeaveLevel(bool disconnectWaiting)
		{
			_inLevel = false;
			ClearState();
			Forget(disconnectWaiting);
		}

		private void Forget(bool disconnectWaiting)
		{
			if (disconnectWaiting)
			{
				foreach (JoinTicket<Il2CppReferenceArray<Object>> item in _tickets.Values.Where(delegate(JoinTicket<Il2CppReferenceArray<Object>> ticket)
				{
					JoinStage stage = ticket.Stage;
					return (uint)stage <= 2u;
				}).ToList())
				{
					_log.Info(item.Name + " is disconnected, the game restarts before they could join");
					GameProtocol.Disconnect(item.ClientId);
				}
			}
			_tickets.Clear();
			_warned.Clear();
			UpdateCodePatches(default(HostState));
		}

		public string Describe()
		{
			HostState hostState = ReadHost();
			List<string> list = new List<string>();
			DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(118, 9);
			defaultInterpolatedStringHandler.AppendLiteral("Hosting ");
			defaultInterpolatedStringHandler.AppendFormatted(hostState.Hosting);
			defaultInterpolatedStringHandler.AppendLiteral(", in a level ");
			defaultInterpolatedStringHandler.AppendFormatted(_inLevel);
			defaultInterpolatedStringHandler.AppendLiteral(", state messages ");
			defaultInterpolatedStringHandler.AppendFormatted(_state.Count);
			defaultInterpolatedStringHandler.AppendLiteral(" + ");
			defaultInterpolatedStringHandler.AppendFormatted(_lasting.Count);
			defaultInterpolatedStringHandler.AppendLiteral(", level ready ");
			defaultInterpolatedStringHandler.AppendFormatted(hostState.LevelReady);
			defaultInterpolatedStringHandler.AppendLiteral(", restarting ");
			defaultInterpolatedStringHandler.AppendFormatted(hostState.Restarting);
			defaultInterpolatedStringHandler.AppendLiteral(", time period ");
			defaultInterpolatedStringHandler.AppendFormatted<GameTimePeriod>((GameTimePeriod)hostState.TimePeriod);
			defaultInterpolatedStringHandler.AppendLiteral(", ");
			defaultInterpolatedStringHandler.AppendLiteral("joining in a level ");
			object value;
			if (_loadingOpen)
			{
				CodePatch loading = _loading;
				if (loading != null && loading.IsFound)
				{
					value = "open";
					goto IL_0114;
				}
			}
			value = "closed";
			goto IL_0114;
			IL_0114:
			defaultInterpolatedStringHandler.AppendFormatted((string?)value);
			defaultInterpolatedStringHandler.AppendLiteral(", player limit ");
			defaultInterpolatedStringHandler.AppendFormatted(_limitWritten);
			list.Add(defaultInterpolatedStringHandler.ToStringAndClear());
			List<string> list2 = list;
			list2.AddRange(_tickets.Values.Select((JoinTicket<Il2CppReferenceArray<Object>> ticket) => $"{ticket.Name} ({ticket.ClientId}): {ticket.Stage} for {Seconds(FrameLoop.Realtime - ticket.StageSince)}, held {ticket.Held.Count}, dropped {ticket.Dropped}, places {ticket.Places?.Count ?? 0}"));
			if (_tickets.Count == 0)
			{
				list2.Add("Nobody is joining");
			}
			foreach (string item in list2)
			{
				_log.Info("[Joins] " + item);
			}
			return list2[0];
		}

		public string SnapshotProbe()
		{
			SnapshotResult snapshotResult = SaveSnapshot.Write();
			string text = "Snapshot written to " + SaveSnapshot.Folder + ": " + snapshotResult;
			_log.Info(text);
			return text;
		}

		private bool ShouldTakeOver(ulong clientId)
		{
			if (_inLevel && Settings != null && Settings.Enabled.Value && GameProtocol.IsHosting)
			{
				return CatNetwork.IsRemotePlayer(clientId);
			}
			return false;
		}

		private void Ensure(ulong clientId)
		{
			if (!_tickets.ContainsKey(clientId))
			{
				JoinTicket<Il2CppReferenceArray<Object>> joinTicket = new JoinTicket<Il2CppReferenceArray<Object>>(clientId, CatNetwork.PlayerName(clientId), FrameLoop.Realtime);
				_tickets[clientId] = joinTicket;
				Notify("message.joining", joinTicket.Name);
				Publish("Connected", joinTicket);
				_log.Info($"{joinTicket.Name} ({clientId}) connected while you are in a level, they wait in the lobby until the game lets them in");
			}
		}

		private void Start(JoinTicket<Il2CppReferenceArray<Object>> ticket)
		{
			try
			{
				SnapshotResult snapshotResult = SaveSnapshot.Write();
				ticket.Places = snapshotResult.Places;
				ticket.MoveTo(JoinStage.Loading, FrameLoop.Realtime);
				GameProtocol.SendSave(ticket.ClientId, snapshotResult.SaveName, snapshotResult.Data);
				GameProtocol.Send(10, ticket.ClientId, GameProtocol.Empty());
				Publish("Loading", ticket, snapshotResult.ToString());
				_log.Info($"{ticket.Name} gets the warehouse as it is now and starts loading. Snapshot {snapshotResult}");
			}
			catch (Exception ex)
			{
				_log.Error("The snapshot for " + ticket.Name + " could not be made", ex);
				Fail(ticket, "the snapshot of the game could not be made");
			}
		}

		private void SendIdentifiers(JoinTicket<Il2CppReferenceArray<Object>> ticket)
		{
			if (ticket.Places == null)
			{
				ticket.Accept(21, reliable: true, null);
				return;
			}
			if (!ticket.PlacesMerged)
			{
				HashSet<uint> saved = new HashSet<uint>(ticket.Places.Select((EntityPlace place) => place.Id));
				ticket.Places = EntityPlaces.Merge(SceneEntities(), ticket.Places);
				ticket.ScenePlaces = ticket.Places.Where((EntityPlace place) => !saved.Contains(place.Id)).ToList();
				ticket.PlacesMerged = true;
			}
			IReadOnlyList<EntityPlace> readOnlyList = ((ticket.Stage == JoinStage.Loading) ? ticket.ScenePlaces : ticket.Places);
			GameProtocol.Send(21, ticket.ClientId, GameProtocol.Identifiers(readOnlyList, NextIdentifier()));
			if (ticket.Stage == JoinStage.Appearing && !ticket.Flushing)
			{
				_log.Info($"{ticket.Name} appeared and has the identifiers of the saved entities now ({ticket.Places.Count}), the state of the warehouse and {ticket.Held.Count} held message(s) follow");
				StartFlush(ticket);
				return;
			}
			if (ticket.Stage != JoinStage.Loading)
			{
				_log.Info($"Sent {ticket.Name} the identifiers of the snapshot again ({ticket.Places.Count})");
				return;
			}
			string text = string.Join(", ", from message in ticket.Held
				group message by message.Code into @group
				orderby @group.Key
				select $"{MessageRecorder.Name(@group.Key)} x{@group.Count()}");
			GameProtocol.Send(11, ticket.ClientId, GameProtocol.Empty());
			ticket.MoveTo(JoinStage.Appearing, FrameLoop.Realtime);
			_log.Info($"{ticket.Name} loaded the level: sent the {readOnlyList.Count} identifiers of the level's own entities, like the game does at this point, and the go to finish loading; {ticket.Held.Count} held message(s) wait until they appear ({((text.Length == 0) ? "none" : text)}), {ticket.Dropped} were dropped");
		}

		private void StartFlush(JoinTicket<Il2CppReferenceArray<Object>> ticket)
		{
			SendState(ticket);
			ticket.Flushing = true;
		}

		private void SendState(JoinTicket<Il2CppReferenceArray<Object>> ticket)
		{
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a7: Unknown result type (might be due to invalid IL or missing references)
			if (ticket.StateSent)
			{
				return;
			}
			ticket.StateSent = true;
			GameTimeManager val = (Singleton<GameTimeManager>.HasInstance() ? Singleton<GameTimeManager>.Instance : null);
			int num = (((Object)(object)val == (Object)null) ? (-1) : ((int)val.CurrentTimePeriod));
			if (num >= 0)
			{
				GameProtocol.Send(45, ticket.ClientId, GameProtocol.TimePeriod(num));
			}
			HashSet<uint> hashSet = new HashSet<uint>((ticket.Places ?? Array.Empty<EntityPlace>()).Select((EntityPlace place) => place.Id));
			IReadOnlyList<LastingMessage<Il2CppReferenceArray<Object>>> readOnlyList = _lasting.For(hashSet.Contains);
			foreach (LastingMessage<Il2CppReferenceArray<Object>> item in readOnlyList)
			{
				GameProtocol.Send(item.Code, ticket.ClientId, item.Payload);
			}
			IReadOnlyList<StateMessage> readOnlyList2 = _state.For(hashSet.Contains);
			foreach (StateMessage item2 in readOnlyList2)
			{
				GameProtocol.Send(38, ticket.ClientId, GameProtocol.Generic(item2.EntityId, item2.Text));
			}
			string text = string.Join(", ", from message in readOnlyList2
				group message by message.Kind into @group
				orderby @group.Count() descending, @group.Key
				select $"{@group.Key} x{@group.Count()}");
			string text2 = string.Join(", ", from message in readOnlyList
				group message by message.Code into @group
				orderby @group.Key
				select $"{MessageRecorder.Name(@group.Key)} x{@group.Count()}");
			_log.Info($"Sent {ticket.Name} the time of day ({((num >= 0) ? ((object)(GameTimePeriod)num/*cast due to .constrained prefix*/).ToString() : "unknown")}) and the state of the warehouse the game sent before they joined: {readOnlyList.Count} of {_lasting.Count} recorded game message(s){((text2.Length == 0) ? string.Empty : (" (" + text2 + ")"))}, {readOnlyList2.Count} of {_state.Count} entity message(s)" + ((text.Length == 0) ? string.Empty : (" (" + text + ")")));
		}

		private void Flush(JoinTicket<Il2CppReferenceArray<Object>> ticket)
		{
			foreach (HeldMessage<Il2CppReferenceArray<Object>> item in ticket.TakeHeld(200))
			{
				GameProtocol.Send(item.Code, ticket.ClientId, item.Payload, item.Reliable);
			}
			if (ticket.Held.Count <= 0)
			{
				ticket.Flushing = false;
				ticket.MoveTo(JoinStage.Synced, FrameLoop.Realtime);
				Publish("Synced", ticket, $"places={ticket.Places.Count} dropped={ticket.Dropped}");
				_log.Info(ticket.Name + " got every held message, the game goes on for them as for everyone");
			}
		}

		private void ClearState()
		{
			_state.Clear();
			_lasting.Clear();
		}

		private static List<EntityPlace> SceneEntities()
		{
			//IL_0071: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Unknown result type (might be due to invalid IL or missing references)
			List<EntityPlace> list = new List<EntityPlace>();
			NetworkedEntityManager obj = (Singleton<NetworkedEntityManager>.HasInstance() ? Singleton<NetworkedEntityManager>.Instance : null);
			List<EntityNetwork> val = ((obj != null) ? obj._initialNetworkedEntities : null);
			if (val == null)
			{
				return list;
			}
			for (int i = 0; i < val.Count; i++)
			{
				EntityNetwork val2 = val[i];
				if (!((Object)(object)val2 == (Object)null) && !((Il2CppObjectBase)val2).WasCollected && Object.op_Implicit((Object)(object)val2))
				{
					Entity linkedEntity = val2.LinkedEntity;
					if (!((Object)(object)linkedEntity != (Object)null) || ((Il2CppObjectBase)linkedEntity).WasCollected || !linkedEntity.IsDisposed)
					{
						Vector3 initialPosition = val2.InitialPosition;
						list.Add(new EntityPlace(val2.NetworkIdentifier, initialPosition.x, initialPosition.y, initialPosition.z));
					}
				}
			}
			return list;
		}

		private static uint NextIdentifier()
		{
			if (!Singleton<NetworkedEntityManager>.HasInstance())
			{
				return 0u;
			}
			return Singleton<NetworkedEntityManager>.Instance._nextIdentifier;
		}

		private void Fail(JoinTicket<Il2CppReferenceArray<Object>> ticket, string reason)
		{
			_tickets.Remove(ticket.ClientId);
			Notify("message.failed", ticket.Name);
			Publish("Failed", ticket, reason);
			_log.Error(ticket.Name + " cannot join: " + reason + "; they are disconnected");
			GameProtocol.Disconnect(ticket.ClientId);
		}

		private void UpdateCodePatches(HostState state)
		{
			WriteStarted();
			int num;
			int num2;
			if (Settings != null)
			{
				num = (Settings.Enabled.Value ? 1 : 0);
				if (num != 0)
				{
					num2 = (JoinGate.CanAccept(state) ? 1 : 0);
					goto IL_002d;
				}
			}
			else
			{
				num = 0;
			}
			num2 = 0;
			goto IL_002d;
			IL_002d:
			bool flag = (byte)num2 != 0;
			CodePatch loading = _loading;
			if (loading != null && loading.IsFound && flag != _loadingOpen && (flag ? _loading.Write(PlayerLimit.SkipJump) : _loading.Restore()))
			{
				_loadingOpen = flag;
				_log.Info((!flag) ? "Joining your level is closed, like in the game" : (CatNetwork.IsWithoutSteamNetwork ? "Joining your level is open, but without the Steam network nobody can reach it" : "Players can join your level now"));
			}
			int num3 = ((num != 0 && Settings != null) ? PlayerLimit.Clamp(Settings.MaxPlayers.Value) : 4);
			loading = _limit;
			if (loading != null && loading.IsFound && num3 != _limitWritten && ((num3 == 4) ? _limit.Restore() : _limit.Write(new byte[1] { PlayerLimit.ConnectionLimit(num3) })))
			{
				_limitWritten = num3;
				_log.Info($"Up to {num3} players can be in your game");
			}
		}

		private static HostState ReadHost()
		{
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			BootstrapManager val = (Singleton<BootstrapManager>.HasInstance() ? Singleton<BootstrapManager>.Instance : null);
			GameTimeManager val2 = (Singleton<GameTimeManager>.HasInstance() ? Singleton<GameTimeManager>.Instance : null);
			return new HostState(GameProtocol.IsHosting, (Object)(object)val != (Object)null && val.LoadFinalized, (Object)(object)val != (Object)null && val.IsRestarting, ((Object)(object)val2 == (Object)null) ? (-1) : ((int)val2.CurrentTimePeriod));
		}

		private static bool IsSpawned(ulong clientId)
		{
			PlayerManager val = (Singleton<PlayerManager>.HasInstance() ? Singleton<PlayerManager>.Instance : null);
			PlayerEntityBase val2 = default(PlayerEntityBase);
			if ((Object)(object)val != (Object)null && val.TryGetPlayer(clientId, ref val2))
			{
				return (Object)(object)val2 != (Object)null;
			}
			return false;
		}

		private void Notify(string key, string name)
		{
			if (Settings == null || Settings.Messages.Value)
			{
				Notifications.Show(_texts.Format(key, new object[1] { name }), (string)null);
			}
		}

		private static void Publish(string step, JoinTicket<Il2CppReferenceArray<Object>> ticket, string details = null)
		{
			GameEventStream.Publish("TooLate." + step, $"client={ticket.ClientId}" + (string.IsNullOrEmpty(details) ? string.Empty : (" " + details)));
		}

		private static string Seconds(double seconds)
		{
			return seconds.ToString("0.0", CultureInfo.InvariantCulture) + " s";
		}
	}
	public sealed class LobbyOverflow
	{
		public const string LabelName = "text_TooLateMorePlayers";

		public const float Gap = 24f;

		public const float LabelWidth = 260f;

		public const float LabelHeight = 60f;

		private readonly CatLogger _log;

		private readonly TextCatalog _texts;

		private readonly List<ulong> _hidden = new List<ulong>();

		private IntPtr _lobby;

		private TMP_Text _label;

		public IReadOnlyList<ulong> Hidden => _hidden;

		public LobbyOverflow(CatLogger log, TextCatalog texts)
		{
			_log = log;
			_texts = texts;
		}

		public void OnJoined(MultiplayerLobbyInterface lobby, ulong steamId)
		{
			Track(lobby);
			if (steamId == 0L || HasSlot(lobby, steamId) || _hidden.Contains(steamId))
			{
				Refresh(lobby);
				return;
			}
			_hidden.Add(steamId);
			_log.Info($"[Lobby] {steamId} has no card in the lobby, {_hidden.Count} player(s) shown as a count");
			Refresh(lobby);
		}

		public void OnLeft(MultiplayerLobbyInterface lobby, ulong steamId)
		{
			Track(lobby);
			_hidden.Remove(steamId);
			if (_hidden.Count > 0 && HasEmptySlot(lobby))
			{
				ulong num = _hidden[0];
				_hidden.RemoveAt(0);
				lobby.ProcessClientJoin(num);
			}
			Refresh(lobby);
		}

		public void Forget()
		{
			_hidden.Clear();
			_lobby = IntPtr.Zero;
			_label = null;
		}

		private void Track(MultiplayerLobbyInterface lobby)
		{
			if ((Object)(object)lobby != (Object)null && ((Il2CppObjectBase)lobby).Pointer != _lobby)
			{
				Forget();
				_lobby = ((Il2CppObjectBase)lobby).Pointer;
			}
		}

		private static bool HasSlot(MultiplayerLobbyInterface lobby, ulong steamId)
		{
			List<ConnectedPlayerHelper> playerSlots = lobby._playerSlots;
			int num = 0;
			while (playerSlots != null && num < playerSlots.Count)
			{
				ConnectedPlayerHelper val = playerSlots[num];
				if ((Object)(object)val != (Object)null && !val.IsEmpty && val.SteamId == steamId)
				{
					return true;
				}
				num++;
			}
			return false;
		}

		private static bool HasEmptySlot(MultiplayerLobbyInterface lobby)
		{
			List<ConnectedPlayerHelper> playerSlots = lobby._playerSlots;
			int num = 0;
			while (playerSlots != null && num < playerSlots.Count)
			{
				if ((Object)(object)playerSlots[num] != (Object)null && playerSlots[num].IsEmpty)
				{
					return true;
				}
				num++;
			}
			return false;
		}

		private void Refresh(MultiplayerLobbyInterface lobby)
		{
			if (_hidden.Count == 0)
			{
				if ((Object)(object)_label != (Object)null && !((Il2CppObjectBase)_label).WasCollected)
				{
					((Component)_label).gameObject.SetActive(false);
				}
				return;
			}
			TMP_Text val = Label(lobby);
			if (!((Object)(object)val == (Object)null))
			{
				val.text = _texts.Plural("lobby.more", (long)_hidden.Count, new object[1] { _hidden.Count });
				((Component)val).gameObject.SetActive(true);
			}
		}

		private TMP_Text Label(MultiplayerLobbyInterface lobby)
		{
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00da: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0116: Unknown result type (might be due to invalid IL or missing references)
			//IL_011b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0125: Unknown result type (might be due to invalid IL or missing references)
			//IL_0130: Unknown result type (might be due to invalid IL or missing references)
			//IL_013f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0144: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)_label != (Object)null && !((Il2CppObjectBase)_label).WasCollected)
			{
				return _label;
			}
			HorizontalLayoutGroup connectedPlayerParent = lobby.ConnectedPlayerParent;
			TMP_Text waitingForHostText = lobby.WaitingForHostText;
			if ((Object)(object)connectedPlayerParent == (Object)null || (Object)(object)waitingForHostText == (Object)null)
			{
				_log.Warning("[Lobby] The lobby has no player row or text to copy, the count of more players is not shown");
				return null;
			}
			RectTransform component = ((Component)connectedPlayerParent).GetComponent<RectTransform>();
			GameObject val = Object.Instantiate<GameObject>(((Component)waitingForHostText).gameObject, ((Transform)component).parent, false);
			((Object)val).name = "text_TooLateMorePlayers";
			_label = val.GetComponent<TMP_Text>();
			_label.enableAutoSizing = false;
			_label.alignment = (TextAlignmentOptions)4097;
			_label.textWrappingMode = (TextWrappingModes)0;
			RectTransform rectTransform = _label.rectTransform;
			Vector2 anchorMin = (rectTransform.anchorMax = component.anchorMin);
			rectTransform.anchorMin = anchorMin;
			rectTransform.pivot = new Vector2(0f, 0.5f);
			rectTransform.sizeDelta = new Vector2(260f, 60f);
			Il2CppStructArray<Vector3> val2 = new Il2CppStructArray<Vector3>(4L);
			component.GetWorldCorners(val2);
			((Transform)rectTransform).position = (((Il2CppArrayBase<Vector3>)(object)val2)[2] + ((Il2CppArrayBase<Vector3>)(object)val2)[3]) * 0.5f;
			rectTransform.anchoredPosition += new Vector2(24f, 0f);
			return _label;
		}
	}
	public sealed class SnapshotResult
	{
		public string SaveName { get; set; }

		public byte[] Data { get; set; }

		public IReadOnlyList<EntityPlace> Places { get; set; }

		public int Stamps { get; set; }

		public bool FromScratch { get; set; }

		public long Milliseconds { get; set; }

		public override string ToString()
		{
			DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(33, 6);
			defaultInterpolatedStringHandler.AppendFormatted(SaveName);
			defaultInterpolatedStringHandler.AppendLiteral(": ");
			byte[] data = Data;
			defaultInterpolatedStringHandler.AppendFormatted((data != null) ? data.Length : 0);
			defaultInterpolatedStringHandler.AppendLiteral(" bytes, ");
			defaultInterpolatedStringHandler.AppendFormatted(Places?.Count ?? 0);
			defaultInterpolatedStringHandler.AppendLiteral(" entities, ");
			defaultInterpolatedStringHandler.AppendFormatted(Stamps);
			defaultInterpolatedStringHandler.AppendLiteral(" stamps");
			defaultInterpolatedStringHandler.AppendFormatted(FromScratch ? ", built without a save file" : string.Empty);
			defaultInterpolatedStringHandler.AppendLiteral(", ");
			defaultInterpolatedStringHandler.AppendFormatted(Milliseconds);
			defaultInterpolatedStringHandler.AppendLiteral(" ms");
			return defaultInterpolatedStringHandler.ToStringAndClear();
		}
	}
	public static class SaveSnapshot
	{
		public const string FolderName = "TooLate";

		public const string FileName = "snapshot.bin";

		public static readonly string[] CopiedTables = new string[3] { "Metadata", "Generic", "ProgressionGeneric" };

		public static string Folder => Path.Combine(Paths.CachePath, "TooLate");

		public static SnapshotResult Write()
		{
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Expected O, but got Unknown
			Stopwatch stopwatch = Stopwatch.StartNew();
			SaveManager val = (Singleton<SaveManager>.HasInstance() ? Singleton<SaveManager>.Instance : null);
			if ((Object)(object)val == (Object)null)
			{
				throw new InvalidOperationException("The game has no save manager");
			}
			string currentSaveFileName = val._currentSaveFileName;
			if (string.IsNullOrEmpty(currentSaveFileName))
			{
				throw new InvalidOperationException("The game has no current save");
			}
			Directory.CreateDirectory(Folder);
			string text = Path.Combine(Folder, "snapshot.bin");
			string gameSaveFullPath = val.GetGameSaveFullPath(currentSaveFileName);
			SqliteConnection.ClearAllPools();
			if (File.Exists(text))
			{
				File.Delete(text);
			}
			bool flag = !File.Exists(gameSaveFullPath);
			List<EntityPlace> list = new List<EntityPlace>();
			int stamps = 0;
			SqliteConnection val2 = new SqliteConnection("Data Source=" + text);
			try
			{
				((DbConnection)val2).Open();
				SqliteCommand command = val2.CreateCommand();
				CreateTables(command);
				if (flag)
				{
					AddMetadata(command, val);
				}
				else
				{
					CopyProgress(command, gameSaveFullPath);
				}
				SqliteTransaction val3 = val2.BeginTransaction();
				SqliteCommand val4 = val2.CreateCommand();
				val4.Transaction = val3;
				if (!val.ExecuteSaveEntities(val4))
				{
					throw new InvalidOperationException("The game could not write its entities into the snapshot");
				}
				if (!val.ExecuteSaveStamps(val4))
				{
					throw new InvalidOperationException("The game could not write its stamps into the snapshot");
				}
				if (!val.ExecuteEnqueuedGenericSaveData(val4))
				{
					throw new InvalidOperationException("The game could not write its progress into the snapshot");
				}
				((DbTransaction)val3).Commit();
				SqliteCommand val5 = val2.CreateCommand();
				((DbCommand)val5).CommandText = "SELECT Id, PositionX, PositionY, PositionZ FROM Entity";
				SqliteDataReader val6 = val5.ExecuteReader();
				while (((DbDataReader)val6).Read())
				{
					if (!((DbDataReader)val6).IsDBNull(0))
					{
						list.Add(new EntityPlace((uint)Number(((DbDataReader)val6).GetValue(0)), (float)Number(((DbDataReader)val6).GetValue(1)), (float)Number(((DbDataReader)val6).GetValue(2)), (float)Number(((DbDataReader)val6).GetValue(3))));
					}
				}
				((DbDataReader)val6).Close();
				((DbCommand)val5).CommandText = "SELECT COUNT(*) FROM Stamps";
				SqliteDataReader val7 = val5.ExecuteReader();
				if (((DbDataReader)val7).Read())
				{
					stamps = (int)Number(((DbDataReader)val7).GetValue(0));
				}
				((DbDataReader)val7).Close();
			}
			finally
			{
				((DbConnection)val2).Close();
				SqliteConnection.ClearAllPools();
			}
			return new SnapshotResult
			{
				SaveName = currentSaveFileName,
				Data = File.ReadAllBytes(text),
				Places = list,
				Stamps = stamps,
				FromScratch = flag,
				Milliseconds = stopwatch.ElapsedMilliseconds
			};
		}

		private static void CreateTables(SqliteCommand command)
		{
			Execute(command, SaveManager.CreateMetadataTable);
			Execute(command, SaveManager.CreateEntityTable);
			Execute(command, SaveManager.CreateStampsTable);
			Execute(command, SaveManager.CreateGameGenericTable);
			Execute(command, SaveManager.CreateGameProgressionGenericTable);
		}

		private static void CopyProgress(SqliteCommand command, string source)
		{
			Execute(command, "ATTACH DATABASE '" + Quote(source) + "' AS original");
			try
			{
				Execute(command, "INSERT INTO Metadata (CreationGameVersion, SaveName) SELECT CreationGameVersion, SaveName FROM original.Metadata");
				Execute(command, "INSERT INTO Generic (Key, ValueNumeric, ValueString, ValueBlob) SELECT Key, ValueNumeric, ValueString, ValueBlob FROM original.Generic");
				Execute(command, "INSERT INTO ProgressionGeneric (Key, ValueNumeric, ValueString, ValueBlob) SELECT Key, ValueNumeric, ValueString, ValueBlob FROM original.ProgressionGeneric");
			}
			finally
			{
				Execute(command, "DETACH DATABASE original");
			}
		}

		private static void AddMetadata(SqliteCommand command, SaveManager save)
		{
			string text = (Singleton<BootstrapManager>.HasInstance() ? Singleton<BootstrapManager>.Instance.GameVersion : string.Empty);
			((DbCommand)command).CommandText = "INSERT INTO Metadata (CreationGameVersion, SaveName) VALUES ('" + Quote(text) + "', '" + Quote(save._currentSaveDisplayName) + "')";
			((DbCommand)command).ExecuteNonQuery();
		}

		private static void Execute(SqliteCommand command, string text)
		{
			((DbCommand)command).CommandText = text;
			((DbCommand)command).ExecuteNonQuery();
		}

		private static string Quote(string text)
		{
			return (text ?? string.Empty).Replace("'", "''");
		}

		private static double Number(Object value)
		{
			if (value == null)
			{
				return 0.0;
			}
			Type il2CppType = value.GetIl2CppType();
			return ((il2CppType != null) ? ((MemberInfo)il2CppType).Name : null) switch
			{
				"DBNull" => 0.0, 
				"Int64" => ((Il2CppObjectBase)value).Unbox<long>(), 
				"Int32" => ((Il2CppObjectBase)value).Unbox<int>(), 
				"Double" => ((Il2CppObjectBase)value).Unbox<double>(), 
				"Single" => ((Il2CppObjectBase)value).Unbox<float>(), 
				_ => double.Parse(value.ToString(), CultureInfo.InvariantCulture), 
			};
		}
	}
}
namespace TooLate.Research
{
	public sealed class MessageRecorder
	{
		public const double SummarySeconds = 5.0;

		private readonly CatLogger _log;

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

		private double _summaryAt;

		public bool IsEnabled { get; private set; }

		public MessageRecorder(CatLogger log)
		{
			_log = log;
		}

		public void SetEnabled(bool enabled)
		{
			if (IsEnabled != enabled)
			{
				if (!enabled)
				{
					Summarize();
				}
				IsEnabled = enabled;
				_summaryAt = FrameLoop.Realtime + 5.0;
				_log.Info(enabled ? "[Messages] Recording the game's network messages: every message once, moving and animation messages as counts every 5 seconds" : "[Messages] Recording stopped");
			}
		}

		public void HostSent(int code, bool reliable, ulong target, Il2CppReferenceArray<Object> payload)
		{
			Record($"host -> {target}", code, reliable, payload);
		}

		public void ClientSent(int code, bool reliable, Il2CppReferenceArray<Object> payload)
		{
			Record("client -> host", code, reliable, payload);
		}

		public void Update()
		{
			if (IsEnabled && FrameLoop.Realtime >= _summaryAt)
			{
				Summarize();
				_summaryAt = FrameLoop.Realtime + 5.0;
			}
		}

		private void Record(string direction, int code, bool reliable, Il2CppReferenceArray<Object> payload)
		{
			if (MessageRoute.IsContinuous(code) || code == 44)
			{
				string key = direction + " " + Name(code);
				_quiet[key] = ((!_quiet.TryGetValue(key, out var value)) ? 1 : (value + 1));
				return;
			}
			_log.Info(string.Format(CultureInfo.InvariantCulture, "[Messages] {0:0.000} f{1} {2}: {3} ({4}){5}, {6}", FrameLoop.Realtime, FrameLoop.FrameCount, direction, Name(code), code, reliable ? string.Empty : " unreliable", GameProtocol.Describe(payload)));
		}

		private void Summarize()
		{
			if (_quiet.Count != 0)
			{
				_log.Info("[Messages] Last seconds: " + string.Join(", ", from pair in _quiet.OrderBy<KeyValuePair<string, int>, string>((KeyValuePair<string, int> pair) => pair.Key, StringComparer.Ordinal)
					select $"{pair.Key} x{pair.Value}"));
				_quiet.Clear();
			}
		}

		public static string Name(int code)
		{
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			if (!Enum.IsDefined(typeof(ProtocolCode), code))
			{
				return "Code" + code;
			}
			return ((object)(ProtocolCode)code/*cast due to .constrained prefix*/).ToString();
		}
	}
}
namespace TooLate.Patches
{
	public static class TooLatePatches
	{
		private static JoinCoordinator _joins;

		private static LobbyOverflow _lobby;

		private static MessageRecorder _recorder;

		public static CatPatches Group { get; private set; }

		public static CatPatches Install(string ownerId, JoinCoordinator joins, LobbyOverflow lobby, MessageRecorder recorder, CatLogger log)
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			_joins = joins;
			_lobby = lobby;
			_recorder = recorder;
			Group = new CatPatches(ownerId, log).Prefix(typeof(Server), "SendMessage", new Type[4]
			{
				typeof(ProtocolCode),
				typeof(bool),
				typeof(ulong),
				typeof(Il2CppReferenceArray<Object>)
			}, typeof(TooLatePatches), "ServerSendPrefix").Prefix(typeof(Server), "BroadcastMessage", new Type[4]
			{
				typeof(ProtocolCode),
				typeof(bool),
				typeof(Nullable<ulong>),
				typeof(Il2CppReferenceArray<Object>)
			}, typeof(TooLatePatches), "ServerBroadcastPrefix").Prefix(typeof(Client), "SendMessage", new Type[3]
			{
				typeof(ProtocolCode),
				typeof(bool),
				typeof(Il2CppReferenceArray<Object>)
			}, typeof(TooLatePatches), "ClientSendPrefix")
				.Prefix(typeof(SaveManager), "NetworkManager_OnClientConnected", new Type[1] { typeof(ulong) }, typeof(TooLatePatches), "SaveOnConnectPrefix")
				.Postfix(typeof(MultiplayerLobbyInterface), "ProcessClientJoin", new Type[1] { typeof(ulong) }, typeof(TooLatePatches), "LobbyJoinPostfix")
				.Postfix(typeof(MultiplayerLobbyInterface), "NetworkManager_OnClientDisconnected", new Type[1] { typeof(ulong) }, typeof(TooLatePatches), "LobbyLeavePostfix");
			Group.Apply();
			return Group;
		}

		private static bool ServerSendPrefix(ProtocolCode __0, bool __1, ulong __2, Il2CppReferenceArray<Object> __3)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Expected I4, but got Unknown
			if (Group == null || !Group.IsActive)
			{
				return true;
			}
			try
			{
				int code = (int)__0;
				if (_recorder.IsEnabled)
				{
					_recorder.HostSent(code, __1, __2, __3);
				}
				if (GameProtocol.IsSending)
				{
					return true;
				}
				_joins.Observe(code, __3);
				return _joins.RouteOutgoing(code, __1, __2, __3);
			}
			catch (Exception ex)
			{
				Group.Fault("ServerSendPrefix", ex);
				return true;
			}
		}

		private static void ServerBroadcastPrefix(ProtocolCode __0, Il2CppReferenceArray<Object> __3)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Expected I4, but got Unknown
			if (Group == null || !Group.IsActive || GameProtocol.IsSending)
			{
				return;
			}
			try
			{
				_joins.Observe((int)__0, __3);
			}
			catch (Exception ex)
			{
				Group.Fault("ServerBroadcastPrefix", ex);
			}
		}

		private static void ClientSendPrefix(ProtocolCode __0, bool __1, Il2CppReferenceArray<Object> __2)
		{
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Expected I4, but got Unknown
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Expected I4, but got Unknown
			if (Group == null || !Group.IsActive)
			{
				return;
			}
			try
			{
				if (_recorder.IsEnabled)
				{
					_recorder.ClientSent((int)__0, __1, __2);
				}
				_joins.Observe((int)__0, __2);
			}
			catch (Exception ex)
			{
				Group.Fault("ClientSendPrefix", ex);
			}
		}

		private static bool SaveOnConnectPrefix(ulong __0)
		{
			if (Group == null || !Group.IsActive)
			{
				return true;
			}
			try
			{
				return !_joins.TakesOverSave(__0);
			}
			catch (Exception ex)
			{
				Group.Fault("SaveOnConnectPrefix", ex);
				return true;
			}
		}

		private static void LobbyJoinPostfix(MultiplayerLobbyInterface __instance, ulong __0)
		{
			if (Group == null || !Group.IsActive)
			{
				return;
			}
			try
			{
				_lobby.OnJoined(__instance, __0);
			}
			catch (Exception ex)
			{
				Group.Fault("LobbyJoinPostfix", ex);
			}
		}

		private static void LobbyLeavePostfix(MultiplayerLobbyInterface __instance, ulong __0)
		{
			if (Group == null || !Group.IsActive)
			{
				return;
			}
			try
			{
				_lobby.OnLeft(__instance, __0);
			}
			catch (Exception ex)
			{
				Group.Fault("LobbyLeavePostfix", ex);
			}
		}
	}
}
namespace TooLate.Logic
{
	public readonly struct EntityPlace
	{
		public uint Id { get; }

		public float X { get; }

		public float Y { get; }

		public float Z { get; }

		public EntityPlace(uint id, float x, float y, float z)
		{
			Id = id;
			X = x;
			Y = y;
			Z = z;
		}

		public override string ToString()
		{
			return $"{Id} ({X:0.###}, {Y:0.###}, {Z:0.###})";
		}
	}
	public static class EntityPlaces
	{
		public const uint EndMark = uint.MaxValue;

		public static IReadOnlyList<EntityPlace> Merge(IEnumerable<EntityPlace> scene, IReadOnlyList<EntityPlace> saved)
		{
			HashSet<uint> hashSet = new HashSet<uint>(saved.Select((EntityPlace place) => place.Id));
			List<EntityPlace> list = new List<EntityPlace>();
			HashSet<uint> hashSet2 = new HashSet<uint>();
			foreach (EntityPlace item in scene)
			{
				if (item.Id != 0 && item.Id != uint.MaxValue && !hashSet.Contains(item.Id) && hashSet2.Add(item.Id))
				{
					list.Add(item);
				}
			}
			HashSet<uint> hashSet3 = new HashSet<uint>();
			foreach (EntityPlace item2 in saved)
			{
				if (item2.Id != 0 && item2.Id != uint.MaxValue && hashSet3.Add(item2.Id))
				{
					list.Add(item2);
				}
			}
			return list;
		}
	}
	public readonly struct HostState
	{
		public bool Hosting { get; }

		public bool LevelReady { get; }

		public bool Restarting { get; }

		public int TimePeriod { get; }

		public HostState(bool hosting, bool levelReady, bool restarting, int timePeriod)
		{
			Hosting = hosting;
			LevelReady = levelReady;
			Restarting = restarting;
			TimePeriod = timePeriod;
		}
	}
	public static class JoinGate
	{
		public const int EveningRecap = 2;

		public const int DawnRecap = 5;

		public const double SettleSeconds = 3.0;

		public static bool CanAccept(HostState state)
		{
			if (state.Hosting && state.LevelReady)
			{
				return !state.Restarting;
			}
			return false;
		}

		public static bool CanStart(HostState state)
		{
			if (CanAccept(state))
			{
				return !IsRecap(state.TimePeriod);
			}
			return false;
		}

		public static bool ReadyToStart(HostState state, double secondsConnected)
		{
			if (CanStart(state))
			{
				return secondsConnected >= 3.0;
			}
			return false;
		}

		public static bool IsRecap(int timePeriod)
		{
			if (timePeriod == 2 || timePeriod == 5)
			{
				return true;
			}
			return false;
		}
	}
	public enum JoinStage
	{
		Waiting,
		Loading,
		Appearing,
		Synced,
		Playing
	}
	public readonly struct HeldMessage<T>
	{
		public int Code { get; }

		public bool Reliable { get; }

		public T Payload { get; }

		public HeldMessage(int code, bool reliable, T payload)
		{
			Code = code;
			Reliable = reliable;
			Payload = payload;
		}
	}
	public sealed class JoinTicket<T>
	{
		public const int MostHeldMessages = 20000;

		private readonly List<HeldMessage<T>> _held = new List<HeldMessage<T>>();

		public ulong ClientId { get; }

		public string Name { get; }

		public double ConnectedAt { get; }

		public JoinStage Stage { get; private set; }

		public double StageSince { get; private set; }

		public IReadOnlyList<EntityPlace> Places { get; set; }

		public int Dropped { get; private set; }

		public int Overflowed { get; private set; }

		public bool WaitingNoticeShown { get; set; }

		public bool Flushing { get; set; }

		public bool PlacesMerged { get; set; }

		public bool StateSent { get; set; }

		public IReadOnlyList<EntityPlace> ScenePlaces { get; set; }

		public string FailReason { get; set; }

		public IReadOnlyList<HeldMessage<T>> Held => _held;

		public JoinTicket(ulong clientId, string name, double now)
		{
			ClientId = clientId;
			Name = name;
			ConnectedAt = now;
			StageSince = now;
		}

		public void MoveTo(JoinStage stage, double now)
		{
			Stage = stage;
			StageSince = now;
		}

		public Route Accept(int code, bool reliable, T payload)
		{
			Route route = MessageRoute.For(Stage, code);
			switch (route)
			{
			case Route.Drop:
				Dropped++;
				break;
			case Route.Hold:
				Hold(code, reliable, payload);
				break;
			case Route.HoldLatest:
				_held.RemoveAll((HeldMessage<T> message) => message.Code == code);
				Hold(code, reliable, payload);
				break;
			}
			return route;
		}

		public List<HeldMessage<T>> TakeHeld()
		{
			return TakeHeld(int.MaxValue);
		}

		public List<HeldMessage<T>> TakeHeld(int most)
		{
			int count = Math.Min(Math.Max(0, most), _held.Count);
			List<HeldMessage<T>> range = _held.GetRange(0, count);
			_held.RemoveRange(0, count);
			return range;
		}

		private void Hold(int code, bool reliable, T payload)
		{
			if (_held.Count >= 20000)
			{
				Overflowed++;
			}
			else
			{
				_held.Add(new HeldMessage<T>(code, reliable, payload));
			}
		}
	}
	public readonly struct LastingMessage<T>
	{
		public int Code { get; }

		public uint EntityId { get; }

		public T Payload { get; }

		public LastingMessage(int code, uint entityId, T payload)
		{
			Code = code;
			EntityId = entityId;
			Payload = payload;
		}
	}
	public sealed class LastingMessages<T>
	{
		public const int MostMessages = 20000;

		private readonly Dictionary<(int Code, uint EntityId), (long Order, LastingMessage<T> Message)> _latest = new Dictionary<(int, uint), (long, LastingMessage<T>)>();

		private long _order;

		public int Count => _latest.Count;

		public static bool IsLasting(int code)
		{
			if ((uint)(code - 48) <= 2u)
			{
				return true;
			}
			return false;
		}

		public static bool IsPerEntity(int code)
		{
			return code != 50;
		}

		public bool Record(int code, uint entityId, T payload)
		{
			if (!IsLasting(code))
			{
				return false;
			}
			if (!IsPerEntity(code))
			{
				entityId = 0u;
			}
			else if (entityId == 0)
			{
				return false;
			}
			(int, uint) key = (code, entityId);
			if (!_latest.ContainsKey(key) && _latest.Count >= 20000)
			{
				return false;
			}
			_latest[key] = (_order++, new LastingMessage<T>(code, entityId, payload));
			return true;
		}

		public int Forget(uint entityId)
		{
			if (entityId == 0)
			{
				return 0;
			}
			List<(int, uint)> list = _latest.Keys.Where(((int Code, uint EntityId) key) => key.EntityId == entityId).ToList();
			foreach (var item in list)
			{
				_latest.Remove(item);
			}
			return list.Count;
		}

		public void Clear()
		{
			_latest.Clear();
			_order = 0L;
		}

		public IReadOnlyList<LastingMessage<T>> For(Func<uint, bool> known)
		{
			return (from entry in _latest.Values
				where entry.Message.EntityId == 0 || known == null || known(entry.Message.EntityId)
				orderby entry.Order
				select entry.Message).ToList();
		}
	}
	public enum Route
	{
		Pass,
		Drop,
		Hold,
		HoldLatest,
		Replace
	}
	public static class MessageRoute
	{
		public static Route For(JoinStage stage, int code)
		{
			if (code == 21)
			{
				return Route.Replace;
			}
			bool flag = (uint)(stage - 3) <= 1u;
			if (flag || IsJoinFlow(code))
			{
				return Route.Pass;
			}
			if (stage == JoinStage.Appearing && code == 20)
			{
				return Route.Pass;
			}
			flag = IsContinuous(code);
			if (!flag)
			{
				bool flag2 = ((code == 20 || code == 38 || code == 51) ? true : false);
				flag = flag2;
			}
			if (flag)
			{
				return Route.Drop;
			}
			if (stage == JoinStage.Waiting)
			{
				return Route.Drop;
			}
			if (code != 44)
			{
				return Route.Hold;
			}
			return Route.HoldLatest;
		}

		public static bool IsJoinFlow(int code)
		{
			switch (code)
			{
			case 3:
			case 4:
			case 5:
			case 6:
			case 9:
			case 10:
			case 11:
			case 13:
			case 28:
			case 29:
				return true;
			default:
				return false;
			}
		}

		public static bool IsContinuous(int code)
		{
			if ((uint)(code - 22) <= 1u || (uint)(code - 26) <= 1u)
			{
				return true;
			}
			return false;
		}
	}
	public static class PlayerLimit
	{
		public const int GamePlayers = 4;

		public const int MostPlayers = 8;

		public const string CountPattern = "E8 ?? ?? ?? ?? 83 F8 05 0F 8D";

		public const int CountOffset = 7;

		public const string LoadingPattern = "44 38 A1 90 00 00 00 0F 85 ?? ?? ?? ??";

		public const int LoadingOffset = 7;

		public const int LoadingLength = 6;

		public static readonly byte[] SkipJump = new byte[6] { 102, 15, 31, 68, 0, 0 };

		public static int Clamp(int players)
		{
			return Math.Max(4, Math.Min(8, players));
		}

		public static byte ConnectionLimit(int players)
		{
			return (byte)(Clamp(players) + 1);
		}
	}
	public static class ProtocolCodes
	{
		public const int ConnectAck = 3;

		public const int ClientConnected = 4;

		public const int ClientDisconnect = 5;

		public const int ClientDisconnectBroadcast = 6;

		public const int ServerDisconnect = 9;

		public const int ServerGameStart = 10;

		public const int ServerFinalizeLoad = 11;

		public const int GrantPlayerSpawn = 13;

		public const int EntityDisposed = 18;

		public const int PlayerSpawnedBroadcast = 20;

		public const int IdentifiersSynchronization = 21;

		public const int MovementAndRotation = 22;

		public const int MovementAndRotationBroadcast = 23;

		public const int AnimationParameter = 26;

		public const int AnimationParameterBroadcast = 27;

		public const int PlayerName = 28;

		public const int PlayerNameBroadcast = 29;

		public const int GenericMessage = 38;

		public const int GameTimeSynchronization = 44;

		public const int GameTimePeriod = 45;

		public const int SetInteractionsLocked = 48;

		public const int SetParcelDamaged = 49;

		public const int BoatDestinations = 50;

		public const int SaveSynchronization = 51;
	}
	public readonly struct StateMessage
	{
		public uint EntityId { get; }

		public string Text { get; }

		public string Kind { get; }

		public StateMessage(uint entityId, string text, string kind)
		{
			EntityId = entityId;
			Text = text;
			Kind = kind;
		}
	}
	public sealed class StateLedger
	{
		public const int MostMessages = 20000;

		public const string CounterKind = "CustomerCounter";

		public const string CounterOpenKind = "CustomerCounter;OpenState";

		private static readonly char[] Separators = new char[2] { ';', ':' };

		private static readonly HashSet<string> MomentKinds = new HashSet<string>(StringComparer.Ordinal) { "Captain", "Customer", "CustomerAudio", "CustomerAppearance", "CarryParcel" };

		private readonly Dictionary<(uint EntityId, string Kind), (long Order, StateMessage Message)> _latest = new Dictionary<(uint, string), (long, StateMessage)>();

		private long _order;

		public int Count => _latest.Count;

		public static string KindOf(string text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return string.Empty;
			}
			int num = text.IndexOfAny(Separators);
			if (num < 0)
			{
				return text;
			}
			string text2 = text.Substring(0, num);
			if (text2 != "CustomerCounter")
			{
				return text2;
			}
			int num2 = text.IndexOfAny(Separators, num + 1);
			if (num2 >= 0)
			{
				return text.Substring(0, num2);
			}
			return text;
		}

		public static bool IsLasting(string kind)
		{
			if (string.IsNullOrEmpty(kind) || MomentKinds.Contains(kind))
			{
				return false;
			}
			if (kind.StartsWith("CustomerCounter", StringComparison.Ordinal))
			{
				return kind == "CustomerCounter;OpenState";
			}
			return true;
		}

		public bool Record(uint entityId, string text)
		{
			if (entityId == 0 || text == null)
			{
				return false;
			}
			string text2 = KindOf(text);
			if (!IsLasting(text2))
			{
				return false;
			}
			(uint, string) key = (entityId, text2);
			if (!_latest.ContainsKey(key) && _latest.Count >= 20000)
			{
				return false;
			}
			_latest[key] = (_order++, new StateMessage(entityId, text, text2));
			return true;
		}

		public int Forget(uint entityId)
		{
			List<(uint, string)> list = _latest.Keys.Where(((uint EntityId, string Kind) key) => key.EntityId == entityId).ToList();
			foreach (var item in list)
			{
				_latest.Remove(item);
			}
			return list.Count;
		}

		public void Clear()
		{
			_latest.Clear();
			_order = 0L;
		}

		public IReadOnlyList<StateMessage> For(Func<uint, bool> known)
		{
			return (from entry in _latest.Values
				where known == null || known(entry.Message.EntityId)
				orderby entry.Order
				select entry.Message).ToList();
		}
	}
}