Decompiled source of MidGameJoin v0.1.3

plugins/MidGameJoin/MidGameJoin.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using InControl;
using MidGameJoin.Core;
using MidGameJoin.Runtime;
using MidGameJoin.UI;
using Photon.Pun;
using Photon.Realtime;
using RWF;
using RWF.GameModes;
using UnboundLib;
using UnboundLib.GameModes;
using UnboundLib.Networking;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("MidGameJoin")]
[assembly: AssemblyDescription("Late-join spectator add-on for RoundsWithFriends")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MidGameJoin")]
[assembly: ComVisible(false)]
[assembly: AssemblyFileVersion("0.1.3.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = "")]
[assembly: AssemblyVersion("0.1.3.0")]
namespace MidGameJoin
{
	[BepInPlugin("com.thunderstore.midgamejoin", "MidGameJoin", "0.1.3")]
	[BepInDependency("com.willis.rounds.unbound", "3.2.14")]
	[BepInDependency("io.olavim.rounds.rwf", "2.2.2")]
	public sealed class MidGameJoinPlugin : BaseUnityPlugin
	{
		private Harmony harmony;

		private void Awake()
		{
			//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c4: Expected O, but got Unknown
			RWFBridge rWFBridge = new RWFBridge();
			if (RWFBridge.ValidateCompatibility(((BaseUnityPlugin)this).Logger))
			{
				LateJoinManager lateJoinManager = ((Component)this).gameObject.AddComponent<LateJoinManager>();
				lateJoinManager.Initialize(rWFBridge, ((BaseUnityPlugin)this).Logger);
				((Component)this).gameObject.AddComponent<LateJoinPhotonCallbacks>();
				((Component)this).gameObject.AddComponent<JoinabilityController>();
				LateJoinUI lateJoinUI = ((Component)this).gameObject.AddComponent<LateJoinUI>();
				SessionSyncController sessionSyncController = ((Component)this).gameObject.AddComponent<SessionSyncController>();
				sessionSyncController.Initialize(lateJoinManager, rWFBridge, lateJoinUI);
				CatchUpCardController catchUpCardController = ((Component)this).gameObject.AddComponent<CatchUpCardController>();
				catchUpCardController.Initialize(lateJoinUI);
				SpectatorController spectatorController = ((Component)this).gameObject.AddComponent<SpectatorController>();
				spectatorController.Manager = lateJoinManager;
				spectatorController.UI = lateJoinUI;
				lateJoinManager.AttachRuntimeControllers(sessionSyncController, lateJoinUI, catchUpCardController);
				GameModeManager.AddHook("PickEnd", (Func<IGameModeHandler, IEnumerator>)lateJoinManager.OnPickEnd, 0);
				harmony = new Harmony("com.thunderstore.midgamejoin");
				harmony.PatchAll();
				((BaseUnityPlugin)this).Logger.LogInfo((object)("MidGameJoin 0.1.3 protocol " + 100 + " loaded for RoundsWithFriends 2.2.2."));
			}
		}

		private void OnDestroy()
		{
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
}
namespace MidGameJoin.UI
{
	public sealed class LateJoinUI : MonoBehaviour
	{
		private GameObject root;

		private Text statusText;

		private Text catchUpText;

		private RectTransform teamContainer;

		private readonly List<GameObject> teamButtons = new List<GameObject>();

		public void ShowSpectating(string status)
		{
			EnsureCreated();
			root.SetActive(true);
			statusText.text = (string.IsNullOrEmpty(status) ? "Waiting for card picks to finish" : status);
			catchUpText.text = string.Empty;
		}

		public void SetTeams(IReadOnlyList<TeamOption> teams, int? selectedTeamId)
		{
			//IL_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_013b: Expected O, but got Unknown
			//IL_0170: Unknown result type (might be due to invalid IL or missing references)
			//IL_018c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0197: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ac: Unknown result type (might be due to invalid IL or missing references)
			//IL_0109: Unknown result type (might be due to invalid IL or missing references)
			EnsureCreated();
			ClearTeamButtons();
			if (teams == null)
			{
				return;
			}
			for (int i = 0; i < teams.Count; i++)
			{
				TeamOption teamOption = teams[i];
				GameObject val = CreateUiObject("Team " + teamOption.TeamId, (Transform)(object)teamContainer);
				RectTransform component = val.GetComponent<RectTransform>();
				component.anchorMin = new Vector2(0.5f, 1f);
				component.anchorMax = new Vector2(0.5f, 1f);
				component.pivot = new Vector2(0.5f, 1f);
				component.anchoredPosition = new Vector2(0f, (float)(-i) * 42f);
				component.sizeDelta = new Vector2(260f, 36f);
				((Graphic)val.AddComponent<Image>()).color = ((selectedTeamId.HasValue && selectedTeamId.Value == teamOption.TeamId) ? new Color(0.3f, 0.75f, 0.35f, 0.95f) : new Color(0.15f, 0.15f, 0.15f, 0.9f));
				Button obj = val.AddComponent<Button>();
				int capturedTeam = teamOption.TeamId;
				((UnityEvent)obj.onClick).AddListener((UnityAction)delegate
				{
					LateJoinNetwork.RequestTeam(capturedTeam);
				});
				Text obj2 = CreateUiObject("Label", val.transform).AddComponent<Text>();
				obj2.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
				obj2.fontSize = 20;
				obj2.alignment = (TextAnchor)4;
				((Graphic)obj2).color = Color.white;
				obj2.text = teamOption.Label;
				RectTransform rectTransform = ((Graphic)obj2).rectTransform;
				rectTransform.anchorMin = Vector2.zero;
				rectTransform.anchorMax = Vector2.one;
				rectTransform.offsetMin = Vector2.zero;
				rectTransform.offsetMax = Vector2.zero;
				teamButtons.Add(val);
			}
		}

		public void ShowCatchUp(int remaining, int total)
		{
			EnsureCreated();
			ClearTeamButtons();
			root.SetActive(true);
			statusText.text = "Joining next fight";
			catchUpText.text = "Choose " + remaining + " catch-up cards" + ((total > 0) ? (" (" + total + " total)") : string.Empty);
		}

		public void Hide()
		{
			if ((Object)(object)root != (Object)null)
			{
				root.SetActive(false);
			}
		}

		private void EnsureCreated()
		{
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_011d: Unknown result type (might be due to invalid IL or missing references)
			//IL_012c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0159: Unknown result type (might be due to invalid IL or missing references)
			//IL_0168: Unknown result type (might be due to invalid IL or missing references)
			//IL_0194: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01df: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0213: Unknown result type (might be due to invalid IL or missing references)
			//IL_022d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0247: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)root != (Object)null))
			{
				root = CreateUiObject("MidGameJoinUI", ((Component)this).transform);
				Canvas obj = root.AddComponent<Canvas>();
				obj.renderMode = (RenderMode)0;
				obj.sortingOrder = 5000;
				root.AddComponent<CanvasScaler>();
				root.AddComponent<GraphicRaycaster>();
				GameObject val = CreateUiObject("Panel", root.transform);
				RectTransform component = val.GetComponent<RectTransform>();
				component.anchorMin = new Vector2(0.5f, 1f);
				component.anchorMax = new Vector2(0.5f, 1f);
				component.pivot = new Vector2(0.5f, 1f);
				component.anchoredPosition = new Vector2(0f, -20f);
				component.sizeDelta = new Vector2(620f, 330f);
				((Graphic)val.AddComponent<Image>()).color = new Color(0f, 0f, 0f, 0.72f);
				CreateText("Title", val.transform, "SPECTATING — JOINING NEXT ROUND", 27, new Vector2(0f, -22f), new Vector2(580f, 42f)).fontStyle = (FontStyle)1;
				statusText = CreateText("Status", val.transform, "Synchronizing match", 20, new Vector2(0f, -72f), new Vector2(580f, 50f));
				catchUpText = CreateText("CatchUp", val.transform, string.Empty, 20, new Vector2(0f, -122f), new Vector2(580f, 42f));
				GameObject val2 = CreateUiObject("Teams", val.transform);
				teamContainer = val2.GetComponent<RectTransform>();
				teamContainer.anchorMin = new Vector2(0.5f, 1f);
				teamContainer.anchorMax = new Vector2(0.5f, 1f);
				teamContainer.pivot = new Vector2(0.5f, 1f);
				teamContainer.anchoredPosition = new Vector2(0f, -172f);
				teamContainer.sizeDelta = new Vector2(300f, 140f);
			}
		}

		private static Text CreateText(string name, Transform parent, string content, int size, Vector2 position, Vector2 dimensions)
		{
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_0089: Unknown result type (might be due to invalid IL or missing references)
			Text obj = CreateUiObject(name, parent).AddComponent<Text>();
			obj.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
			obj.fontSize = size;
			obj.alignment = (TextAnchor)4;
			((Graphic)obj).color = Color.white;
			obj.text = content;
			RectTransform rectTransform = ((Graphic)obj).rectTransform;
			rectTransform.anchorMin = new Vector2(0.5f, 1f);
			rectTransform.anchorMax = new Vector2(0.5f, 1f);
			rectTransform.pivot = new Vector2(0.5f, 1f);
			rectTransform.anchoredPosition = position;
			rectTransform.sizeDelta = dimensions;
			return obj;
		}

		private static GameObject CreateUiObject(string name, Transform parent)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Expected O, but got Unknown
			GameObject val = new GameObject(name, new Type[1] { typeof(RectTransform) });
			val.transform.SetParent(parent, false);
			return val;
		}

		private void ClearTeamButtons()
		{
			for (int i = 0; i < teamButtons.Count; i++)
			{
				if ((Object)(object)teamButtons[i] != (Object)null)
				{
					Object.Destroy((Object)(object)teamButtons[i]);
				}
			}
			teamButtons.Clear();
		}
	}
	public sealed class TeamOption
	{
		public int TeamId { get; private set; }

		public string Label { get; private set; }

		public TeamOption(int teamId, string label)
		{
			TeamId = teamId;
			Label = label;
		}
	}
}
namespace MidGameJoin.Runtime
{
	public static class CapacityValidator
	{
		public static bool CanInsert(int activePlayers, int rwfMaxPlayers, int modeMaxPlayers)
		{
			int num = Math.Min(rwfMaxPlayers, modeMaxPlayers);
			return activePlayers < num;
		}
	}
	public sealed class CatchUpCardController : MonoBehaviour
	{
		private LateJoinUI ui;

		public void Initialize(LateJoinUI lateJoinUi)
		{
			ui = lateJoinUi;
		}

		public IEnumerator RunCatchUp(Player player, int catchUpCount)
		{
			if ((Object)(object)player == (Object)null || catchUpCount < 0)
			{
				yield break;
			}
			LobbyCharacter character = PlayerExtensions.GetAdditionalData(player).character;
			bool isMine = character != null && character.IsMine;
			if ((Object)(object)CardChoiceVisuals.instance != (Object)null)
			{
				CardChoiceVisuals.instance.Show(player.playerID, true);
			}
			for (int i = 0; i < catchUpCount; i++)
			{
				if (isMine && (Object)(object)ui != (Object)null)
				{
					ui.ShowCatchUp(catchUpCount - i, catchUpCount);
				}
				yield return GameModeManager.TriggerHook("PlayerPickStart");
				yield return CardChoice.instance.DoPick(1, player.playerID, (PickerType)1);
				yield return GameModeManager.TriggerHook("PlayerPickEnd");
			}
			if ((Object)(object)CardChoiceVisuals.instance != (Object)null)
			{
				CardChoiceVisuals.instance.Hide();
			}
			if (isMine && (Object)(object)ui != (Object)null)
			{
				ui.ShowSpectating("Finalizing join");
			}
		}
	}
	public sealed class JoinabilityController : MonoBehaviour
	{
		private void Update()
		{
			LateJoinManager instance = LateJoinManager.Instance;
			if (!((Object)(object)instance == (Object)null) && instance.Bridge != null && instance.Bridge.IsSupportedGameMode() && PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && PhotonNetwork.CurrentRoom != null && !((Object)(object)GameManager.instance == (Object)null) && GameManager.instance.isPlaying)
			{
				if (!PhotonNetwork.CurrentRoom.IsOpen)
				{
					PhotonNetwork.CurrentRoom.IsOpen = true;
				}
				if (PhotonNetwork.CurrentRoom.IsVisible)
				{
					PhotonNetwork.CurrentRoom.IsVisible = false;
				}
			}
		}
	}
	public sealed class LateJoinManager : MonoBehaviour
	{
		private sealed class PickEndDecision
		{
			public int Generation { get; private set; }

			public int BatchId { get; private set; }

			public int CatchUpCount { get; private set; }

			public int[] ActorIds { get; private set; }

			public PickEndDecision(int generation, int batchId, int catchUpCount, int[] actorIds)
			{
				Generation = generation;
				BatchId = batchId;
				CatchUpCount = catchUpCount;
				ActorIds = actorIds;
			}
		}

		private sealed class PlayerCreationDirective
		{
			public int TeamId { get; private set; }

			public int ColorId { get; private set; }

			public PlayerCreationDirective(int teamId, int colorId)
			{
				TeamId = teamId;
				ColorId = colorId;
			}
		}

		private readonly LateJoinCoordinator coordinator = new LateJoinCoordinator();

		private readonly Dictionary<int, LateJoinRecord> mirroredRecords = new Dictionary<int, LateJoinRecord>();

		private readonly Random nonceRandom = new Random();

		private ManualLogSource logger;

		private bool localJoinedRoom;

		private PickEndDecision pickEndDecision;

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

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

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

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

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

		private CatchUpCardController catchUpCards;

		private bool inPickEndHook;

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

		private int localTeamRequestNonce;

		private bool hostMigrationRecoveryRunning;

		public static LateJoinManager Instance { get; private set; }

		public RWFBridge Bridge { get; private set; }

		public int LocalChallengeNonce { get; private set; }

		public int PickEndGeneration { get; set; }

		public SessionSyncController SessionSync { get; private set; }

		public LateJoinUI UI { get; private set; }

		public IReadOnlyList<LateJoinRecord> Records
		{
			get
			{
				if (!PhotonNetwork.IsMasterClient)
				{
					return mirroredRecords.Values.OrderBy((LateJoinRecord r) => r.ActorId).ToArray();
				}
				return coordinator.Records;
			}
		}

		public LateJoinRecord LocalRecord
		{
			get
			{
				if (PhotonNetwork.LocalPlayer == null)
				{
					return null;
				}
				return GetRecord(PhotonNetwork.LocalPlayer.ActorNumber);
			}
		}

		internal LateJoinCoordinator Coordinator => coordinator;

		public void Initialize(RWFBridge bridge, ManualLogSource log)
		{
			Bridge = bridge;
			logger = log;
			LateJoinNetwork.Manager = this;
		}

		public void AttachRuntimeControllers(SessionSyncController sessionSync, LateJoinUI ui, CatchUpCardController catchUpController)
		{
			SessionSync = sessionSync;
			UI = ui;
			catchUpCards = catchUpController;
		}

		private void Awake()
		{
			Instance = this;
		}

		private void OnDestroy()
		{
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
			if ((Object)(object)LateJoinNetwork.Manager == (Object)(object)this)
			{
				LateJoinNetwork.Manager = null;
			}
		}

		public void OnLocalJoinedRoom()
		{
			localJoinedRoom = true;
			LocalChallengeNonce = 0;
			localTeamRequestNonce = 0;
		}

		public void OnRemoteActorEntered(int actorId)
		{
			if (PhotonNetwork.IsMasterClient && !((Object)(object)GameManager.instance == (Object)null) && GameManager.instance.isPlaying && Bridge != null && Bridge.IsSupportedGameMode())
			{
				LateJoinRecord lateJoinRecord = coordinator.Register(actorId, 0);
				if (lateJoinRecord.State != LateJoinState.Disconnected)
				{
					int nonce = (lateJoinRecord.ChallengeNonce = NextPositiveNonce());
					lateJoinRecord.LastTeamNonce = 0;
					lateJoinRecord.Status = "Waiting for MidGameJoin handshake";
					LateJoinNetwork.SendChallenge(actorId, nonce);
					BroadcastRecords();
					LogInfo("Challenge sent to late actor " + actorId + ".");
				}
			}
		}

		public void OnRemoteActorLeft(Player otherPlayer)
		{
			if (otherPlayer == null)
			{
				return;
			}
			int actorNumber = otherPlayer.ActorNumber;
			if (PhotonNetwork.IsMasterClient)
			{
				LateJoinRecord record = coordinator.GetRecord(actorNumber);
				if (record == null || record.State == LateJoinState.Disconnected)
				{
					mirroredRecords.Remove(actorNumber);
					return;
				}
				int batchId = record.BatchId;
				if (record.State != LateJoinState.Active && Bridge != null && (Object)(object)Bridge.FindPlayerOwnedByActor(actorNumber) != (Object)null)
				{
					Bridge.RollBackLatePlayer(otherPlayer);
				}
				if (coordinator.Disconnect(actorNumber))
				{
					if (batchId > 0)
					{
						ReceiveRollback(actorNumber, batchId, "Disconnected during late join");
						LateJoinNetwork.BroadcastRollback(actorNumber, batchId, "Disconnected during late join");
					}
					BroadcastRecords();
					LogInfo("Late actor " + actorNumber + " disconnected.");
				}
			}
			mirroredRecords.Remove(actorNumber);
		}

		public void OnMasterClientSwitched(int actorId)
		{
			LogInfo("Photon master switched to actor " + actorId + ". Pending late-join recovery is deferred until RWF issues NetworkRestart.");
		}

		public void OnRwfHostRestartIssued(int actorId)
		{
			if (PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId)
			{
				BeginHostMigrationRecovery();
			}
		}

		public void OnChallengeReceived(int nonce, int hostProtocolVersion)
		{
			if (localJoinedRoom && nonce > 0)
			{
				LocalChallengeNonce = nonce;
				localTeamRequestNonce = 0;
				if ((Object)(object)UI != (Object)null)
				{
					UI.ShowSpectating("Synchronizing match");
				}
				RefreshLocalUi();
				if (hostProtocolVersion / 100 != 1)
				{
					LogWarning("Host MidGameJoin protocol is incompatible: " + hostProtocolVersion + ".");
				}
				else
				{
					LateJoinNetwork.SendHello(nonce);
				}
			}
		}

		public void OnHello(int actorId, int nonce, int protocolVersion)
		{
			LateJoinRecord record = coordinator.GetRecord(actorId);
			if (record == null || nonce <= 0 || record.ChallengeNonce != nonce)
			{
				LogWarning("Rejected stale/invalid MidGameJoin hello for actor " + actorId + ".");
				return;
			}
			if (protocolVersion / 100 != 1)
			{
				record.ProtocolVersion = protocolVersion;
				record.Status = "MidGameJoin version mismatch";
				LateJoinNetwork.SendHelloRejected(actorId, "MidGameJoin version mismatch");
				BroadcastRecords();
				return;
			}
			record.ProtocolVersion = protocolVersion;
			bool num = coordinator.HandshakeAccepted(actorId);
			bool flag = record.State == LateJoinState.Spectating || record.State == LateJoinState.TeamSelected || record.State == LateJoinState.WaitingForInsertion;
			if (num || flag)
			{
				LateJoinNetwork.SendHelloAccepted(actorId);
				SessionSnapshot sessionSnapshot = Bridge.CaptureSessionSnapshot();
				sessionSnapshot.PickEndGeneration = PickEndGeneration;
				sessionSnapshot.ActivePlayerCount = GetCanonicalActivePlayerCount();
				LateJoinNetwork.SendSessionSnapshot(actorId, record.ChallengeNonce, sessionSnapshot);
			}
			BroadcastRecords();
			LogInfo("HelloAccepted actor=" + actorId + " protocol=" + protocolVersion + ".");
		}

		public void OnHelloAccepted(int actorId, int protocolVersion)
		{
			if (PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId)
			{
				LogInfo("Late join handshake accepted by host; protocol=" + protocolVersion + ".");
			}
		}

		public void OnHelloRejected(int actorId, int hostProtocolVersion, string reason)
		{
			if (PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId)
			{
				LogWarning((reason ?? "MidGameJoin version mismatch") + " (host protocol " + hostProtocolVersion + ").");
			}
		}

		public void OnSessionSnapshot(int challengeNonce, SessionSnapshot snapshot)
		{
			if (challengeNonce <= 0 || challengeNonce != LocalChallengeNonce)
			{
				LogDebug("Ignored stale session snapshot nonce=" + challengeNonce + ".");
			}
			else if ((Object)(object)SessionSync != (Object)null)
			{
				SessionSync.ApplySnapshot(snapshot);
			}
		}

		public void OnSessionReady(int actorId, int challengeNonce, int currentLevelId)
		{
			if (!PhotonNetwork.IsMasterClient)
			{
				return;
			}
			LateJoinRecord record = coordinator.GetRecord(actorId);
			if (record == null || challengeNonce <= 0 || record.ChallengeNonce != challengeNonce || record.State == LateJoinState.Disconnected || record.State == LateJoinState.Active)
			{
				LogDebug("Ignored stale session-ready actor=" + actorId + " nonce=" + challengeNonce + ".");
				return;
			}
			if ((Object)(object)MapManager.instance != (Object)null && currentLevelId != MapManager.instance.currentLevelID)
			{
				record.Status = "Synchronizing match";
				BroadcastRecords();
				return;
			}
			record.SessionReady = true;
			if (Bridge != null && Bridge.IsTeamMode())
			{
				if (record.SelectedTeamId.HasValue && Bridge.GetCurrentTeamIds().Contains(record.SelectedTeamId.Value))
				{
					coordinator.MarkWaiting(actorId);
				}
				else
				{
					record.Status = "Choose a team while spectating";
				}
			}
			else
			{
				coordinator.MarkWaiting(actorId);
			}
			BroadcastRecords();
		}

		public void OnTeamRequested(int actorId, int challengeNonce, int commandNonce, int teamId)
		{
			LateJoinRecord record = coordinator.GetRecord(actorId);
			if (record == null || challengeNonce <= 0 || record.ChallengeNonce != challengeNonce)
			{
				LogDebug("Ignored stale team request for actor " + actorId + ".");
			}
			else
			{
				if (!ValidateTeamSelection(actorId, teamId))
				{
					return;
				}
				if (!coordinator.TrySelectTeamCommand(actorId, commandNonce, teamId))
				{
					LogDebug("Ignored duplicate/stale team command actor=" + actorId + " nonce=" + commandNonce + ".");
				}
				else
				{
					if (record.SessionReady)
					{
						coordinator.MarkWaiting(actorId);
					}
					BroadcastRecords();
				}
			}
		}

		public bool ValidateTeamSelection(int actorId, int teamId)
		{
			if (!PhotonNetwork.IsMasterClient || Bridge == null || !Bridge.IsSupportedGameMode() || !Bridge.IsTeamMode())
			{
				return false;
			}
			LateJoinRecord record = coordinator.GetRecord(actorId);
			if (record == null || !record.SessionReady || record.State == LateJoinState.Active || record.State == LateJoinState.Disconnected)
			{
				return false;
			}
			return Bridge.GetCurrentTeamIds().Contains(teamId);
		}

		public int[] GetBatchEligibleActors()
		{
			if (!PhotonNetwork.IsMasterClient || Bridge == null || !Bridge.IsSupportedGameMode())
			{
				return new int[0];
			}
			int num = (((Object)(object)RWFMod.instance != (Object)null) ? RWFMod.instance.MaxPlayers : 16);
			int modeMaxPlayers = num;
			if (GameModeManager.CurrentHandler != null && GameModeManager.CurrentHandler.Settings != null && GameModeManager.CurrentHandler.Settings.ContainsKey("maxPlayers"))
			{
				object obj = GameModeManager.CurrentHandler.Settings["maxPlayers"];
				if (obj is int)
				{
					modeMaxPlayers = (int)obj;
				}
			}
			HashSet<int> hashSet = new HashSet<int>(Bridge.GetCurrentTeamIds());
			int num2 = ((!((Object)(object)PlayerManager.instance == (Object)null)) ? PlayerManager.instance.players.Count : 0);
			List<int> list = new List<int>();
			foreach (LateJoinRecord item in coordinator.Records.OrderBy((LateJoinRecord r) => r.ActorId))
			{
				if (item.ProtocolVersion / 100 == 1 && item.SessionReady && (item.State == LateJoinState.WaitingForInsertion || item.State == LateJoinState.TeamSelected))
				{
					if (Bridge.IsTeamMode() && (!item.SelectedTeamId.HasValue || !hashSet.Contains(item.SelectedTeamId.Value)))
					{
						item.Status = "Choose a valid team";
						continue;
					}
					if (!CapacityValidator.CanInsert(num2 + list.Count, num, modeMaxPlayers))
					{
						item.Status = "Lobby/player limit reached for this insertion";
						continue;
					}
					list.Add(item.ActorId);
					item.Status = "Waiting for insertion";
				}
			}
			BroadcastRecords();
			return list.ToArray();
		}

		public void ApplyRecordSnapshot(int[] actorIds, int[] states, int[] teamIds, int[] batchIds, int[] catchUpCounts, int[] protocolVersions, string[] statuses)
		{
			if (!PhotonNetwork.IsMasterClient)
			{
				LateJoinRecord[] array;
				try
				{
					array = SnapshotCodec.DecodeRecords(actorIds, states, teamIds, batchIds, catchUpCounts, protocolVersions, statuses);
				}
				catch (Exception ex)
				{
					LogWarning("Rejected malformed MidGameJoin record snapshot: " + ex.Message);
					return;
				}
				mirroredRecords.Clear();
				for (int i = 0; i < array.Length; i++)
				{
					mirroredRecords[array[i].ActorId] = array[i];
				}
				RefreshLocalUi();
			}
		}

		public void RefreshLocalUi()
		{
			if ((Object)(object)UI == (Object)null)
			{
				return;
			}
			LateJoinRecord localRecord = LocalRecord;
			if (localRecord == null || localRecord.State == LateJoinState.Active)
			{
				if (localRecord != null && localRecord.State == LateJoinState.Active)
				{
					UI.Hide();
				}
				return;
			}
			UI.ShowSpectating(localRecord.Status);
			if (localRecord.State == LateJoinState.CatchUpPicking)
			{
				UI.SetTeams(new TeamOption[0], null);
			}
			else if (Bridge != null && Bridge.IsTeamMode())
			{
				TeamOption[] teams = (from id in Bridge.GetCurrentTeamIds()
					orderby id
					select new TeamOption(id, "TEAM " + (id + 1))).ToArray();
				UI.SetTeams(teams, localRecord.SelectedTeamId);
			}
			else
			{
				UI.SetTeams(new TeamOption[0], null);
			}
		}

		public LateJoinRecord GetRecord(int actorId)
		{
			if (PhotonNetwork.IsMasterClient)
			{
				return coordinator.GetRecord(actorId);
			}
			if (!mirroredRecords.TryGetValue(actorId, out var value))
			{
				return null;
			}
			return value;
		}

		public IEnumerator OnPickEnd(IGameModeHandler handler)
		{
			inPickEndHook = true;
			try
			{
				yield return OnPickEndCore(handler);
			}
			finally
			{
				inPickEndHook = false;
			}
		}

		private IEnumerator OnPickEndCore(IGameModeHandler handler)
		{
			PickEndGeneration++;
			int generation = PickEndGeneration;
			pickEndDecision = null;
			creationDirectives.Clear();
			rolledBackActors.Clear();
			catchUpReadyActors.Clear();
			catchUpBeginActors.Clear();
			localCatchUpReadySent.Clear();
			if (Bridge == null || !Bridge.IsSupportedGameMode() || !PhotonNetwork.InRoom || PhotonNetwork.CurrentRoom == null)
			{
				yield break;
			}
			HashSet<int> activeOwners = GetActiveOwnerActorIds();
			if (!PhotonNetwork.CurrentRoom.Players.Keys.Any((int item) => !activeOwners.Contains(item)))
			{
				yield break;
			}
			if (PhotonNetwork.IsMasterClient)
			{
				int catchUpCount = LateJoinRules.CalculateCatchUpCount(Bridge.GetExistingCardCounts().ToArray());
				int[] batchEligibleActors = GetBatchEligibleActors();
				InsertionBatch insertionBatch = coordinator.LockBatch(batchEligibleActors, catchUpCount);
				ReceivePickEndDecision(generation, insertionBatch.Id, insertionBatch.CatchUpCount, insertionBatch.ActorIds.ToArray());
				LateJoinNetwork.BroadcastPickEndDecision(generation, insertionBatch.Id, insertionBatch.CatchUpCount, insertionBatch.ActorIds.ToArray());
				BroadcastRecords();
			}
			while (pickEndDecision == null || pickEndDecision.Generation != generation)
			{
				if (!PhotonNetwork.InRoom)
				{
					yield break;
				}
				yield return null;
			}
			if (pickEndDecision.BatchId <= 0 || pickEndDecision.ActorIds.Length == 0)
			{
				yield break;
			}
			for (int index = 0; index < pickEndDecision.ActorIds.Length; index++)
			{
				int actorId = pickEndDecision.ActorIds[index];
				if (PhotonNetwork.IsMasterClient)
				{
					if (PhotonNetwork.CurrentRoom == null || !PhotonNetwork.CurrentRoom.Players.ContainsKey(actorId))
					{
						coordinator.Disconnect(actorId);
						RollBackActor(actorId, pickEndDecision.BatchId, "Disconnected before player creation", cleanPlayerObjects: false);
					}
					else
					{
						LateJoinRecord record = coordinator.GetRecord(actorId);
						int teamId = ((Bridge.IsTeamMode() && record != null && record.SelectedTeamId.HasValue) ? record.SelectedTeamId.Value : Bridge.GetNextFfaTeamId());
						int colorId = (Bridge.IsTeamMode() ? Bridge.GetColorForTeam(teamId) : Bridge.GetAvailableColorId());
						ReceiveBeginPlayerCreation(actorId, pickEndDecision.BatchId, teamId, colorId);
						LateJoinNetwork.BroadcastBeginPlayerCreation(actorId, pickEndDecision.BatchId, teamId, colorId);
					}
				}
				while (!HasDirective(actorId, pickEndDecision.BatchId) && !IsRolledBack(actorId, pickEndDecision.BatchId))
				{
					if (!PhotonNetwork.InRoom)
					{
						yield break;
					}
					yield return null;
				}
				if (IsRolledBack(actorId, pickEndDecision.BatchId))
				{
					continue;
				}
				PlayerCreationDirective playerCreationDirective = creationDirectives[DirectiveKey(actorId, pickEndDecision.BatchId)];
				yield return Bridge.CreateLatePlayer(actorId, 0, playerCreationDirective.TeamId, playerCreationDirective.ColorId);
				Player player = Bridge.FindPlayerOwnedByActor(actorId);
				if (PhotonNetwork.IsMasterClient)
				{
					if ((Object)(object)player == (Object)null)
					{
						RollBackActor(actorId, pickEndDecision.BatchId, "Player creation timed out", cleanPlayerObjects: true);
					}
					else
					{
						LateJoinRecord record2 = coordinator.GetRecord(actorId);
						if (record2 != null && record2.BatchId == pickEndDecision.BatchId)
						{
							record2.StartingCardCount = (((Object)(object)player.data != (Object)null && player.data.currentCards != null) ? player.data.currentCards.Count : 0);
							record2.State = LateJoinState.CatchUpPicking;
							record2.Status = ((pickEndDecision.CatchUpCount == 0) ? "Finalizing join" : "Choosing catch-up cards");
							Bridge.EnsurePlayerJoined(player);
							BroadcastRecords();
						}
					}
				}
				while (!IsRolledBack(actorId, pickEndDecision.BatchId) && !IsActorInState(actorId, LateJoinState.CatchUpPicking) && (!PhotonNetwork.IsMasterClient || !IsActorResolved(actorId, pickEndDecision.BatchId)))
				{
					yield return null;
				}
				if (IsRolledBack(actorId, pickEndDecision.BatchId))
				{
					continue;
				}
				player = Bridge.FindPlayerOwnedByActor(actorId);
				if ((Object)(object)player == (Object)null)
				{
					if (PhotonNetwork.IsMasterClient)
					{
						RollBackActor(actorId, pickEndDecision.BatchId, "Player view missing before catch-up", cleanPlayerObjects: true);
					}
					while (!IsActorResolved(actorId, pickEndDecision.BatchId))
					{
						yield return null;
					}
					continue;
				}
				Bridge.EnsurePlayerJoined(player);
				yield return WaitForCatchUpBegin(actorId, pickEndDecision.BatchId);
				if (IsRolledBack(actorId, pickEndDecision.BatchId))
				{
					continue;
				}
				if ((Object)(object)catchUpCards != (Object)null)
				{
					yield return catchUpCards.RunCatchUp(player, pickEndDecision.CatchUpCount);
				}
				if (PhotonNetwork.IsMasterClient)
				{
					LateJoinRecord record3 = coordinator.GetRecord(actorId);
					int num = (((Object)(object)player.data != (Object)null && player.data.currentCards != null) ? player.data.currentCards.Count : 0);
					if (record3 != null && record3.BatchId == pickEndDecision.BatchId && num >= record3.StartingCardCount + record3.CatchUpCount)
					{
						if (record3.CatchUpCount == 0)
						{
							record3.CatchUpComplete = true;
						}
						else
						{
							coordinator.MarkCatchUpComplete(actorId, pickEndDecision.BatchId);
						}
						if (coordinator.Activate(actorId, pickEndDecision.BatchId))
						{
							record3.ChallengeNonce = 0;
							record3.LastTeamNonce = 0;
						}
						BroadcastRecords();
					}
					else
					{
						RollBackActor(actorId, pickEndDecision.BatchId, "Catch-up card count did not synchronize", cleanPlayerObjects: true);
					}
				}
				while (!IsActorResolved(actorId, pickEndDecision.BatchId))
				{
					if (!PhotonNetwork.InRoom)
					{
						yield break;
					}
					yield return null;
				}
			}
			if ((Object)(object)PlayerManager.instance != (Object)null)
			{
				PlayerManagerExtensions.ResetPickOrder(PlayerManager.instance);
			}
		}

		public void ReceivePickEndDecision(int generation, int batchId, int catchUpCount, int[] actorIds)
		{
			if (!inPickEndHook && generation > PickEndGeneration)
			{
				creationDirectives.Clear();
				rolledBackActors.Clear();
				catchUpReadyActors.Clear();
				catchUpBeginActors.Clear();
				localCatchUpReadySent.Clear();
			}
			if (inPickEndHook)
			{
				if (generation != PickEndGeneration)
				{
					return;
				}
			}
			else
			{
				if (generation < PickEndGeneration)
				{
					return;
				}
				PickEndGeneration = generation;
			}
			pickEndDecision = new PickEndDecision(generation, batchId, catchUpCount, actorIds ?? new int[0]);
		}

		public void ReceiveBeginPlayerCreation(int actorId, int batchId, int teamId, int colorId)
		{
			if (pickEndDecision == null || pickEndDecision.BatchId != batchId || !pickEndDecision.ActorIds.Contains(actorId))
			{
				return;
			}
			string text = DirectiveKey(actorId, batchId);
			if (!IsRolledBack(actorId, batchId))
			{
				PlayerCreationDirective playerCreationDirective = new PlayerCreationDirective(teamId, colorId);
				creationDirectives[text] = playerCreationDirective;
				if (!inPickEndHook && PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId && Bridge != null && (Object)(object)Bridge.FindPlayerOwnedByActor(actorId) == (Object)null && !detachedLocalFlows.Contains(text) && !IsActorResolved(actorId, batchId))
				{
					detachedLocalFlows.Add(text);
					((MonoBehaviour)this).StartCoroutine(RunDetachedLocalActorFlow(actorId, batchId, playerCreationDirective));
				}
			}
		}

		public void OnCatchUpReady(int actorId, int batchId, int nonce)
		{
			if (!PhotonNetwork.IsMasterClient || batchId <= 0 || nonce <= 0)
			{
				return;
			}
			LateJoinRecord record = coordinator.GetRecord(actorId);
			if (record != null && record.BatchId == batchId && record.State == LateJoinState.CatchUpPicking && record.ChallengeNonce == nonce && PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players.ContainsKey(actorId))
			{
				string item = DirectiveKey(actorId, batchId);
				if (catchUpReadyActors.Add(item))
				{
					ReceiveCatchUpBegin(actorId, batchId);
					LateJoinNetwork.BroadcastCatchUpBegin(actorId, batchId);
				}
			}
		}

		public void ReceiveCatchUpBegin(int actorId, int batchId)
		{
			if (batchId > 0)
			{
				catchUpBeginActors.Add(DirectiveKey(actorId, batchId));
			}
		}

		public void ReceiveRollback(int actorId, int batchId, string reason)
		{
			rolledBackActors.Add(DirectiveKey(actorId, batchId));
			if (!PhotonNetwork.IsMasterClient && (Object)(object)UI != (Object)null && PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId)
			{
				UI.ShowSpectating(reason);
			}
		}

		private IEnumerator RunDetachedLocalActorFlow(int actorId, int batchId, PlayerCreationDirective directive)
		{
			string key = DirectiveKey(actorId, batchId);
			yield return Bridge.CreateLatePlayer(actorId, 0, directive.TeamId, directive.ColorId);
			while (!IsRolledBack(actorId, batchId) && !IsActorInState(actorId, LateJoinState.CatchUpPicking) && !IsActorResolved(actorId, batchId))
			{
				yield return null;
			}
			if (!IsRolledBack(actorId, batchId) && IsActorInState(actorId, LateJoinState.CatchUpPicking))
			{
				Player player = Bridge.FindPlayerOwnedByActor(actorId);
				if ((Object)(object)player != (Object)null)
				{
					yield return WaitForCatchUpBegin(actorId, batchId);
					if (!IsRolledBack(actorId, batchId) && (Object)(object)catchUpCards != (Object)null)
					{
						yield return catchUpCards.RunCatchUp(player, (pickEndDecision != null) ? pickEndDecision.CatchUpCount : 0);
					}
				}
			}
			detachedLocalFlows.Remove(key);
		}

		private IEnumerator WaitForCatchUpBegin(int actorId, int batchId)
		{
			string key = DirectiveKey(actorId, batchId);
			if (PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId && !localCatchUpReadySent.Contains(key))
			{
				localCatchUpReadySent.Add(key);
				LateJoinNetwork.SendCatchUpReady(actorId, batchId, LocalChallengeNonce);
			}
			while (!catchUpBeginActors.Contains(key) && !IsRolledBack(actorId, batchId) && PhotonNetwork.InRoom)
			{
				yield return null;
			}
		}

		private HashSet<int> GetActiveOwnerActorIds()
		{
			HashSet<int> hashSet = new HashSet<int>();
			if ((Object)(object)PlayerManager.instance == (Object)null)
			{
				return hashSet;
			}
			foreach (Player player in PlayerManager.instance.players)
			{
				if (!((Object)(object)player == (Object)null) && !((Object)(object)player.data == (Object)null) && !((Object)(object)player.data.view == (Object)null))
				{
					hashSet.Add(player.data.view.OwnerActorNr);
				}
			}
			return hashSet;
		}

		private int GetCanonicalActivePlayerCount()
		{
			if ((Object)(object)PlayerManager.instance == (Object)null)
			{
				return 0;
			}
			int num = 0;
			foreach (Player player in PlayerManager.instance.players)
			{
				if (!((Object)(object)player == (Object)null) && !((Object)(object)player.data == (Object)null) && !((Object)(object)player.data.view == (Object)null))
				{
					LateJoinRecord record = coordinator.GetRecord(player.data.view.OwnerActorNr);
					if (record == null || record.State == LateJoinState.Active)
					{
						num++;
					}
				}
			}
			return num;
		}

		private void RollBackActor(int actorId, int batchId, string reason, bool cleanPlayerObjects)
		{
			if (PhotonNetwork.IsMasterClient)
			{
				if (cleanPlayerObjects && Bridge != null)
				{
					Bridge.RollBackLatePlayer(actorId);
				}
				LateJoinRecord record = coordinator.GetRecord(actorId);
				if (record != null && record.State != LateJoinState.Disconnected)
				{
					coordinator.RollBackToSpectator(actorId, reason);
				}
				ReceiveRollback(actorId, batchId, reason);
				LateJoinNetwork.BroadcastRollback(actorId, batchId, reason);
				BroadcastRecords();
			}
		}

		private bool HasDirective(int actorId, int batchId)
		{
			return creationDirectives.ContainsKey(DirectiveKey(actorId, batchId));
		}

		private bool IsRolledBack(int actorId, int batchId)
		{
			return rolledBackActors.Contains(DirectiveKey(actorId, batchId));
		}

		private bool IsActorInState(int actorId, LateJoinState state)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record != null)
			{
				return record.State == state;
			}
			return false;
		}

		private bool IsActorResolved(int actorId, int batchId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null)
			{
				return IsRolledBack(actorId, batchId);
			}
			if (record.State != LateJoinState.Active && record.State != LateJoinState.Disconnected)
			{
				if (record.State == LateJoinState.Spectating)
				{
					return record.BatchId == 0;
				}
				return false;
			}
			return true;
		}

		private static string DirectiveKey(int actorId, int batchId)
		{
			return actorId + ":" + batchId;
		}

		private void Update()
		{
		}

		private void CheckSpectatorExclusionInvariant()
		{
			if (Bridge == null)
			{
				return;
			}
			foreach (LateJoinRecord record in Records)
			{
				if (record != null && record.State != LateJoinState.Active && record.State != LateJoinState.Disconnected && record.BatchId == 0 && record.State != LateJoinState.CatchUpPicking && (Object)(object)Bridge.FindPlayerOwnedByActor(record.ActorId) != (Object)null)
				{
					LogError("Spectator exclusion invariant violated for actor " + record.ActorId + ".");
				}
			}
		}

		private void BeginHostMigrationRecovery()
		{
			if (!hostMigrationRecoveryRunning && PhotonNetwork.IsMasterClient)
			{
				hostMigrationRecoveryRunning = true;
				((MonoBehaviour)this).StartCoroutine(RecoverPendingAfterHostMigration());
			}
		}

		private IEnumerator RecoverPendingAfterHostMigration()
		{
			try
			{
				yield return null;
				yield return null;
				float deadline = Time.realtimeSinceStartup + 8f;
				while (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && (Bridge == null || !Bridge.IsSupportedGameMode() || (Object)(object)GameManager.instance == (Object)null || !GameManager.instance.isPlaying) && Time.realtimeSinceStartup < deadline)
				{
					yield return null;
				}
				if (!PhotonNetwork.InRoom || !PhotonNetwork.IsMasterClient || PhotonNetwork.CurrentRoom == null)
				{
					yield break;
				}
				LateJoinRecord[] array = (from r in mirroredRecords.Values
					where r != null && r.State != LateJoinState.Active && r.State != LateJoinState.Disconnected
					orderby r.ActorId
					select r).ToArray();
				HashSet<int> hashSet = ((Bridge != null && Bridge.IsTeamMode()) ? new HashSet<int>(Bridge.GetCurrentTeamIds()) : new HashSet<int>());
				int lastObservedBatchId = (from r in mirroredRecords.Values
					where r != null
					select r.BatchId).DefaultIfEmpty(0).Max();
				coordinator.ClearAndAdvanceBatchSequence(lastObservedBatchId);
				pickEndDecision = null;
				creationDirectives.Clear();
				rolledBackActors.Clear();
				catchUpReadyActors.Clear();
				catchUpBeginActors.Clear();
				localCatchUpReadySent.Clear();
				LateJoinRecord[] array2 = array;
				foreach (LateJoinRecord lateJoinRecord in array2)
				{
					if (PhotonNetwork.CurrentRoom.Players.ContainsKey(lateJoinRecord.ActorId))
					{
						if (Bridge != null && (Object)(object)Bridge.FindPlayerOwnedByActor(lateJoinRecord.ActorId) != (Object)null)
						{
							Bridge.RollBackLatePlayer(lateJoinRecord.ActorId);
						}
						bool flag = !Bridge.IsTeamMode() || (lateJoinRecord.SelectedTeamId.HasValue && hashSet.Contains(lateJoinRecord.SelectedTeamId.Value));
						LateJoinRecord lateJoinRecord2 = coordinator.Register(lateJoinRecord.ActorId, lateJoinRecord.ProtocolVersion);
						lateJoinRecord2.SelectedTeamId = (flag ? lateJoinRecord.SelectedTeamId : ((int?)null));
						coordinator.ResetPendingForHostMigration(lateJoinRecord.ActorId, "Recovering after host migration", flag);
						LateJoinNetwork.SendChallenge(nonce: lateJoinRecord2.ChallengeNonce = NextPositiveNonce(), actorId: lateJoinRecord.ActorId);
					}
				}
				mirroredRecords.Clear();
				BroadcastRecords();
				LogInfo("Recovered " + array.Length + " pending late-join record(s) after host migration.");
			}
			finally
			{
				hostMigrationRecoveryRunning = false;
			}
		}

		public int NextLocalTeamRequestNonce()
		{
			if (localTeamRequestNonce == int.MaxValue)
			{
				localTeamRequestNonce = 0;
			}
			localTeamRequestNonce++;
			return localTeamRequestNonce;
		}

		public void BroadcastRecords()
		{
			if (PhotonNetwork.IsMasterClient)
			{
				LateJoinNetwork.BroadcastRecords(SnapshotCodec.EncodeRecords(coordinator.Records));
			}
		}

		private int NextPositiveNonce()
		{
			lock (nonceRandom)
			{
				return nonceRandom.Next(1, int.MaxValue);
			}
		}

		private void LogInfo(string message)
		{
			if (logger != null)
			{
				logger.LogInfo((object)message);
			}
		}

		private void LogWarning(string message)
		{
			if (logger != null)
			{
				logger.LogWarning((object)message);
			}
		}

		private void LogDebug(string message)
		{
			if (logger != null)
			{
				logger.LogDebug((object)message);
			}
		}

		private void LogError(string message)
		{
			if (logger != null)
			{
				logger.LogError((object)message);
			}
		}
	}
	public static class LateJoinNetwork
	{
		public static LateJoinManager Manager { get; set; }

		public static void SendChallenge(int actorId, int nonce)
		{
			SendToActor(actorId, "RPCT_Challenge", actorId, nonce, 100);
		}

		public static void SendHello(int nonce)
		{
			if (PhotonNetwork.LocalPlayer != null)
			{
				NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCH_Hello", new object[3]
				{
					PhotonNetwork.LocalPlayer.ActorNumber,
					nonce,
					100
				});
			}
		}

		public static void SendHelloAccepted(int actorId)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCA_HelloAccepted", new object[2] { actorId, 100 });
		}

		public static void SendHelloRejected(int actorId, string reason)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCA_HelloRejected", new object[3]
			{
				actorId,
				100,
				reason ?? string.Empty
			});
		}

		public static void RequestTeam(int teamId)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.LocalPlayer != null)
			{
				NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCH_SelectTeam", new object[4]
				{
					PhotonNetwork.LocalPlayer.ActorNumber,
					Manager.LocalChallengeNonce,
					Manager.NextLocalTeamRequestNonce(),
					teamId
				});
			}
		}

		public static void SendSessionSnapshot(int actorId, int challengeNonce, SessionSnapshot snapshot)
		{
			if (snapshot != null && challengeNonce > 0)
			{
				SendToActor(actorId, "RPCT_SessionSnapshot", actorId, challengeNonce, snapshot.GameModeId ?? string.Empty, snapshot.CurrentLevelId, snapshot.BattleOngoing, snapshot.IsTransitioning, snapshot.PickEndGeneration, snapshot.ActivePlayerCount, snapshot.TeamIds ?? new int[0], snapshot.TeamPoints ?? new int[0], snapshot.TeamRounds ?? new int[0]);
			}
		}

		public static void SendSessionReady(int challengeNonce, int currentLevelId)
		{
			if (PhotonNetwork.LocalPlayer != null && challengeNonce > 0)
			{
				NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCH_SessionReady", new object[3]
				{
					PhotonNetwork.LocalPlayer.ActorNumber,
					challengeNonce,
					currentLevelId
				});
			}
		}

		public static void BroadcastPickEndDecision(int generation, int batchId, int catchUpCount, int[] actorIds)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCA_PickEndDecision", new object[4]
			{
				generation,
				batchId,
				catchUpCount,
				actorIds ?? new int[0]
			});
		}

		public static void BroadcastBeginPlayerCreation(int actorId, int batchId, int teamId, int colorId)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCT_BeginPlayerCreation", new object[4] { actorId, batchId, teamId, colorId });
		}

		public static void SendCatchUpReady(int actorId, int batchId, int nonce)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCH_CatchUpReady", new object[3] { actorId, batchId, nonce });
		}

		public static void BroadcastCatchUpBegin(int actorId, int batchId)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCA_CatchUpBegin", new object[2] { actorId, batchId });
		}

		public static void BroadcastRollback(int actorId, int batchId, string reason)
		{
			NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCA_RollBackActor", new object[3]
			{
				actorId,
				batchId,
				reason ?? string.Empty
			});
		}

		public static void BroadcastRecords(EncodedRecords snapshot)
		{
			if (snapshot != null)
			{
				NetworkingManager.RPC(typeof(LateJoinNetwork), "RPCA_RecordSnapshot", new object[7] { snapshot.ActorIds, snapshot.States, snapshot.TeamIds, snapshot.BatchIds, snapshot.CatchUpCounts, snapshot.ProtocolVersions, snapshot.Statuses });
			}
		}

		[UnboundRPC]
		public static void RPCT_Challenge(int targetActorId, int nonce, int hostProtocolVersion)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == targetActorId)
			{
				Manager.OnChallengeReceived(nonce, hostProtocolVersion);
			}
		}

		[UnboundRPC]
		public static void RPCH_Hello(int actorId, int nonce, int protocolVersion)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.IsMasterClient)
			{
				Manager.OnHello(actorId, nonce, protocolVersion);
			}
		}

		[UnboundRPC]
		public static void RPCA_HelloAccepted(int actorId, int protocolVersion)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.OnHelloAccepted(actorId, protocolVersion);
			}
		}

		[UnboundRPC]
		public static void RPCA_HelloRejected(int actorId, int hostProtocolVersion, string reason)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.OnHelloRejected(actorId, hostProtocolVersion, reason);
			}
		}

		[UnboundRPC]
		public static void RPCH_SelectTeam(int actorId, int challengeNonce, int commandNonce, int teamId)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.IsMasterClient)
			{
				Manager.OnTeamRequested(actorId, challengeNonce, commandNonce, teamId);
			}
		}

		[UnboundRPC]
		public static void RPCA_PickEndDecision(int generation, int batchId, int catchUpCount, int[] actorIds)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.ReceivePickEndDecision(generation, batchId, catchUpCount, actorIds);
			}
		}

		[UnboundRPC]
		public static void RPCT_BeginPlayerCreation(int actorId, int batchId, int teamId, int colorId)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.ReceiveBeginPlayerCreation(actorId, batchId, teamId, colorId);
			}
		}

		[UnboundRPC]
		public static void RPCH_CatchUpReady(int actorId, int batchId, int nonce)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.IsMasterClient)
			{
				Manager.OnCatchUpReady(actorId, batchId, nonce);
			}
		}

		[UnboundRPC]
		public static void RPCA_CatchUpBegin(int actorId, int batchId)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.ReceiveCatchUpBegin(actorId, batchId);
			}
		}

		[UnboundRPC]
		public static void RPCA_RollBackActor(int actorId, int batchId, string reason)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.ReceiveRollback(actorId, batchId, reason);
			}
		}

		[UnboundRPC]
		public static void RPCT_SessionSnapshot(int targetActorId, int challengeNonce, string gameModeId, int currentLevelId, bool battleOngoing, bool isTransitioning, int pickEndGeneration, int activePlayerCount, int[] teamIds, int[] teamPoints, int[] teamRounds)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == targetActorId)
			{
				Manager.OnSessionSnapshot(challengeNonce, new SessionSnapshot
				{
					GameModeId = gameModeId,
					CurrentLevelId = currentLevelId,
					BattleOngoing = battleOngoing,
					IsTransitioning = isTransitioning,
					PickEndGeneration = pickEndGeneration,
					ActivePlayerCount = activePlayerCount,
					TeamIds = teamIds,
					TeamPoints = teamPoints,
					TeamRounds = teamRounds
				});
			}
		}

		[UnboundRPC]
		public static void RPCH_SessionReady(int actorId, int challengeNonce, int currentLevelId)
		{
			if (!((Object)(object)Manager == (Object)null) && PhotonNetwork.IsMasterClient)
			{
				Manager.OnSessionReady(actorId, challengeNonce, currentLevelId);
			}
		}

		[UnboundRPC]
		public static void RPCA_RecordSnapshot(int[] actorIds, int[] states, int[] teamIds, int[] batchIds, int[] catchUpCounts, int[] protocolVersions, string[] statuses)
		{
			if ((Object)(object)Manager != (Object)null)
			{
				Manager.ApplyRecordSnapshot(actorIds, states, teamIds, batchIds, catchUpCounts, protocolVersions, statuses);
			}
		}

		private static void SendToActor(int actorId, string methodName, params object[] data)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			RaiseEventOptions val = new RaiseEventOptions();
			val.TargetActors = new int[1] { actorId };
			RaiseEventOptions val2 = val;
			NetworkingManager.RPC(typeof(LateJoinNetwork), methodName, val2, SendOptions.SendReliable, data);
		}
	}
	public sealed class LateJoinPhotonCallbacks : MonoBehaviourPunCallbacks
	{
		public override void OnJoinedRoom()
		{
			if ((Object)(object)LateJoinManager.Instance != (Object)null)
			{
				LateJoinManager.Instance.OnLocalJoinedRoom();
			}
		}

		public override void OnPlayerEnteredRoom(Player newPlayer)
		{
			if (newPlayer != null && !((Object)(object)LateJoinManager.Instance == (Object)null) && PhotonNetwork.IsMasterClient && (Object)(object)GameManager.instance != (Object)null && GameManager.instance.isPlaying)
			{
				LateJoinManager.Instance.OnRemoteActorEntered(newPlayer.ActorNumber);
			}
		}

		public override void OnPlayerLeftRoom(Player otherPlayer)
		{
			if (otherPlayer != null && (Object)(object)LateJoinManager.Instance != (Object)null)
			{
				LateJoinManager.Instance.OnRemoteActorLeft(otherPlayer);
			}
		}

		public override void OnMasterClientSwitched(Player newMasterClient)
		{
			if (newMasterClient != null && (Object)(object)LateJoinManager.Instance != (Object)null)
			{
				LateJoinManager.Instance.OnMasterClientSwitched(newMasterClient.ActorNumber);
			}
		}
	}
	public sealed class RWFBridge
	{
		public const string SupportedRwfVersion = "2.2.2";

		public const float CreationTimeoutSeconds = 12f;

		private static readonly BindingFlags RwfFieldFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

		private static FieldInfo teamPointsField;

		private static FieldInfo teamRoundsField;

		private static FieldInfo isTransitioningField;

		private static MethodInfo createPlayerMethod;

		public static bool Compatible { get; private set; }

		public static bool ValidateCompatibility(ManualLogSource logger)
		{
			createPlayerMethod = typeof(PlayerAssignerExtensions).GetMethod("CreatePlayer", BindingFlags.Static | BindingFlags.Public, null, new Type[3]
			{
				typeof(PlayerAssigner),
				typeof(LobbyCharacter),
				typeof(InputDevice)
			}, null);
			teamPointsField = typeof(RWFGameMode).GetField("teamPoints", RwfFieldFlags);
			teamRoundsField = typeof(RWFGameMode).GetField("teamRounds", RwfFieldFlags);
			isTransitioningField = typeof(RWFGameMode).GetField("isTransitioning", RwfFieldFlags);
			Compatible = string.Equals("2.2.2", "2.2.2", StringComparison.Ordinal) && createPlayerMethod != null && teamPointsField != null && teamRoundsField != null && isTransitioningField != null;
			if (!Compatible && logger != null)
			{
				logger.LogError((object)"MidGameJoin disabled: incompatible RoundsWithFriends runtime. Expected RWF 2.2.2 with CreatePlayer(PlayerAssigner, LobbyCharacter, InputDevice), teamPoints, teamRounds and isTransitioning. Detected RWF 2.2.2.");
			}
			return Compatible;
		}

		public bool IsSupportedGameMode()
		{
			if (!Compatible || GameModeManager.CurrentHandler == null || !(GameModeManager.CurrentHandler.GameMode is RWFGameMode))
			{
				return false;
			}
			if (!string.Equals(GameModeManager.CurrentHandlerID, "Deathmatch", StringComparison.Ordinal))
			{
				return string.Equals(GameModeManager.CurrentHandlerID, "Team Deathmatch", StringComparison.Ordinal);
			}
			return true;
		}

		public bool IsTeamMode()
		{
			if (IsSupportedGameMode())
			{
				return GameModeManager.CurrentHandler.AllowTeams;
			}
			return false;
		}

		public IReadOnlyList<int> GetCurrentTeamIds()
		{
			if ((Object)(object)PlayerManager.instance == (Object)null)
			{
				return new int[0];
			}
			return (from id in (from p in PlayerManager.instance.players
					where (Object)(object)p != (Object)null
					select p.teamID).Distinct()
				orderby id
				select id).ToArray();
		}

		public IReadOnlyList<int> GetExistingCardCounts()
		{
			if ((Object)(object)PlayerManager.instance == (Object)null)
			{
				return new int[0];
			}
			return (from p in PlayerManager.instance.players
				where (Object)(object)p != (Object)null && (Object)(object)p.data != (Object)null && p.data.currentCards != null
				select p.data.currentCards.Count).ToArray();
		}

		public SessionSnapshot CaptureSessionSnapshot()
		{
			RWFGameMode rwfGameMode = GetRwfGameMode();
			Dictionary<int, int> points = ReadScoreDictionary(rwfGameMode, teamPointsField);
			Dictionary<int, int> rounds = ReadScoreDictionary(rwfGameMode, teamRoundsField);
			int[] array = (from id in points.Keys.Concat(rounds.Keys).Distinct()
				orderby id
				select id).ToArray();
			return new SessionSnapshot
			{
				GameModeId = (GameModeManager.CurrentHandlerID ?? string.Empty),
				CurrentLevelId = (((Object)(object)MapManager.instance != (Object)null) ? MapManager.instance.currentLevelID : (-1)),
				BattleOngoing = ((Object)(object)GameManager.instance != (Object)null && GameManager.instance.battleOngoing),
				IsTransitioning = ((Object)(object)rwfGameMode != (Object)null && isTransitioningField != null && (bool)isTransitioningField.GetValue(rwfGameMode)),
				PickEndGeneration = 0,
				ActivePlayerCount = (((Object)(object)PlayerManager.instance != (Object)null) ? PlayerManager.instance.players.Count : 0),
				TeamIds = array,
				TeamPoints = array.Select((int id) => points.ContainsKey(id) ? points[id] : 0).ToArray(),
				TeamRounds = array.Select((int id) => rounds.ContainsKey(id) ? rounds[id] : 0).ToArray()
			};
		}

		public IEnumerator ApplySessionSnapshot(SessionSnapshot snapshot)
		{
			if (snapshot == null)
			{
				yield break;
			}
			RWFGameMode rwfGameMode = GetRwfGameMode();
			if (!((Object)(object)rwfGameMode == (Object)null) && snapshot.TeamIds != null && snapshot.TeamPoints != null && snapshot.TeamRounds != null && snapshot.TeamIds.Length == snapshot.TeamPoints.Length && snapshot.TeamIds.Length == snapshot.TeamRounds.Length)
			{
				Dictionary<int, int> dictionary = new Dictionary<int, int>();
				Dictionary<int, int> dictionary2 = new Dictionary<int, int>();
				for (int i = 0; i < snapshot.TeamIds.Length; i++)
				{
					dictionary[snapshot.TeamIds[i]] = snapshot.TeamPoints[i];
					dictionary2[snapshot.TeamIds[i]] = snapshot.TeamRounds[i];
				}
				teamPointsField.SetValue(rwfGameMode, dictionary);
				teamRoundsField.SetValue(rwfGameMode, dictionary2);
				isTransitioningField.SetValue(rwfGameMode, snapshot.IsTransitioning);
				if ((Object)(object)UIHandler.instance != (Object)null)
				{
					UIHandlerExtensions.ShowRoundCounterSmall(UIHandler.instance, dictionary, dictionary2);
				}
				yield return null;
			}
		}

		public IEnumerator CreateLatePlayer(int actorId, int localId, int teamId, int colorId)
		{
			if (Compatible && PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players.TryGetValue(actorId, out var value) && value != null)
			{
				if (PhotonNetwork.LocalPlayer != null && PhotonNetwork.LocalPlayer.ActorNumber == actorId)
				{
					LobbyCharacter val = new LobbyCharacter(value, colorId, localId)
					{
						teamID = teamId
					};
					yield return PlayerAssignerExtensions.CreatePlayer(PlayerAssigner.instance, val, (InputDevice)null);
				}
				float deadline = Time.realtimeSinceStartup + 12f;
				while ((Object)(object)FindPlayerOwnedByActor(actorId) == (Object)null && Time.realtimeSinceStartup < deadline)
				{
					yield return null;
				}
			}
		}

		public Player FindPlayerOwnedByActor(int actorId)
		{
			if ((Object)(object)PlayerManager.instance == (Object)null)
			{
				return null;
			}
			foreach (Player player in PlayerManager.instance.players)
			{
				if (!((Object)(object)player == (Object)null))
				{
					LobbyCharacter character = PlayerExtensions.GetAdditionalData(player).character;
					if (character != null && character.actorID == actorId)
					{
						return player;
					}
				}
			}
			return null;
		}

		public void EnsurePlayerJoined(Player player)
		{
			if (!((Object)(object)player == (Object)null) && GameModeManager.CurrentHandler != null)
			{
				RWFGameMode rwfGameMode = GetRwfGameMode();
				Dictionary<int, int> dictionary = ReadScoreDictionary(rwfGameMode, teamPointsField);
				Dictionary<int, int> dictionary2 = ReadScoreDictionary(rwfGameMode, teamRoundsField);
				if (!dictionary.ContainsKey(player.teamID) || !dictionary2.ContainsKey(player.teamID))
				{
					GameModeManager.CurrentHandler.PlayerJoined(player);
				}
			}
		}

		public void RollBackLatePlayer(int actorId)
		{
			if (PhotonNetwork.IsMasterClient && PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Players.TryGetValue(actorId, out var value) && value != null)
			{
				RollBackLatePlayer(value);
			}
		}

		public void RollBackLatePlayer(Player networkPlayer)
		{
			if (PhotonNetwork.IsMasterClient && networkPlayer != null)
			{
				PhotonNetwork.DestroyPlayerObjects(networkPlayer);
			}
		}

		public int GetColorForTeam(int teamId)
		{
			if ((Object)(object)PlayerManager.instance != (Object)null)
			{
				foreach (Player player in PlayerManager.instance.players)
				{
					if (!((Object)(object)player == (Object)null) && player.teamID == teamId)
					{
						LobbyCharacter character = PlayerExtensions.GetAdditionalData(player).character;
						if (character != null)
						{
							return NormalizeColorId(character.colorID);
						}
					}
				}
			}
			return NormalizeColorId(teamId);
		}

		public int GetNextFfaTeamId()
		{
			HashSet<int> hashSet = new HashSet<int>(GetCurrentTeamIds());
			int i;
			for (i = 0; hashSet.Contains(i); i++)
			{
			}
			return i;
		}

		public int GetAvailableColorId()
		{
			int num = Math.Max(1, RWFMod.MaxColorsHardLimit);
			HashSet<int> hashSet = new HashSet<int>();
			if ((Object)(object)PlayerManager.instance != (Object)null)
			{
				foreach (Player player in PlayerManager.instance.players)
				{
					if (!((Object)(object)player == (Object)null))
					{
						LobbyCharacter character = PlayerExtensions.GetAdditionalData(player).character;
						if (character != null)
						{
							hashSet.Add(NormalizeColorId(character.colorID));
						}
					}
				}
			}
			for (int i = 0; i < num; i++)
			{
				if (!hashSet.Contains(i))
				{
					return i;
				}
			}
			return NormalizeColorId(((Object)(object)PlayerManager.instance != (Object)null) ? PlayerManager.instance.players.Count : 0);
		}

		private static RWFGameMode GetRwfGameMode()
		{
			if (GameModeManager.CurrentHandler != null)
			{
				MonoBehaviour gameMode = GameModeManager.CurrentHandler.GameMode;
				return (RWFGameMode)(object)((gameMode is RWFGameMode) ? gameMode : null);
			}
			return null;
		}

		private static Dictionary<int, int> ReadScoreDictionary(RWFGameMode gameMode, FieldInfo field)
		{
			if ((Object)(object)gameMode == (Object)null || field == null)
			{
				return new Dictionary<int, int>();
			}
			if (field.GetValue(gameMode) is Dictionary<int, int> dictionary)
			{
				return new Dictionary<int, int>(dictionary);
			}
			return new Dictionary<int, int>();
		}

		private static int NormalizeColorId(int colorId)
		{
			int num = Math.Max(1, RWFMod.MaxColorsHardLimit);
			int num2 = colorId % num;
			if (num2 >= 0)
			{
				return num2;
			}
			return num2 + num;
		}
	}
	public sealed class SessionSyncController : MonoBehaviour
	{
		private const float PlayerSyncTimeoutSeconds = 10f;

		private LateJoinManager manager;

		private RWFBridge bridge;

		private LateJoinUI ui;

		private Coroutine bootstrapRoutine;

		public void Initialize(LateJoinManager lateJoinManager, RWFBridge rwfBridge, LateJoinUI lateJoinUi)
		{
			manager = lateJoinManager;
			bridge = rwfBridge;
			ui = lateJoinUi;
		}

		public void ApplySnapshot(SessionSnapshot snapshot)
		{
			if (bootstrapRoutine != null)
			{
				((MonoBehaviour)this).StopCoroutine(bootstrapRoutine);
			}
			bootstrapRoutine = ((MonoBehaviour)this).StartCoroutine(Bootstrap(snapshot));
		}

		private IEnumerator Bootstrap(SessionSnapshot snapshot)
		{
			if (snapshot == null || bridge == null || string.IsNullOrEmpty(snapshot.GameModeId))
			{
				yield break;
			}
			ui.ShowSpectating("Synchronizing match");
			if (GameModeManager.CurrentHandlerID != snapshot.GameModeId)
			{
				GameModeManager.SetGameMode(snapshot.GameModeId);
			}
			float handlerDeadline = Time.realtimeSinceStartup + 10f;
			while (GameModeManager.CurrentHandler == null && Time.realtimeSinceStartup < handlerDeadline)
			{
				yield return null;
			}
			if (GameModeManager.CurrentHandler == null)
			{
				yield break;
			}
			if ((Object)(object)GameManager.instance != (Object)null)
			{
				GameManager.instance.isPlaying = true;
				GameManager.instance.battleOngoing = false;
			}
			yield return bridge.ApplySessionSnapshot(snapshot);
			float playerDeadline = Time.realtimeSinceStartup + 10f;
			while ((Object)(object)PlayerManager.instance != (Object)null && !SessionSyncRules.HasAllActivePlayers(PlayerManager.instance.players.Count, snapshot.ActivePlayerCount) && Time.realtimeSinceStartup < playerDeadline)
			{
				yield return null;
			}
			if (!SessionSyncRules.HasAllActivePlayers((!((Object)(object)PlayerManager.instance == (Object)null)) ? PlayerManager.instance.players.Count : 0, snapshot.ActivePlayerCount))
			{
				ui.ShowSpectating("Resynchronizing active players");
				LateJoinNetwork.SendHello(manager.LocalChallengeNonce);
				bootstrapRoutine = null;
				yield break;
			}
			if ((Object)(object)MapManager.instance != (Object)null && snapshot.CurrentLevelId >= 0)
			{
				MapManager.instance.currentLevelID = snapshot.CurrentLevelId;
				MapManager.instance.CallInNewMapAndMovePlayers(snapshot.CurrentLevelId);
			}
			if ((Object)(object)PlayerManager.instance != (Object)null)
			{
				PlayerManager.instance.SetPlayersSimulated(snapshot.BattleOngoing);
				ExtensionMethods.InvokeMethod((object)PlayerManager.instance, "SetPlayersVisible", new object[1] { true });
			}
			manager.PickEndGeneration = snapshot.PickEndGeneration;
			LateJoinNetwork.SendSessionReady(manager.LocalChallengeNonce, snapshot.CurrentLevelId);
			ui.ShowSpectating("Waiting for card picks to finish");
			bootstrapRoutine = null;
		}
	}
	public sealed class SpectatorController : MonoBehaviour
	{
		public LateJoinManager Manager { get; set; }

		public LateJoinUI UI { get; set; }

		private void Update()
		{
			if ((Object)(object)Manager == (Object)null || PhotonNetwork.LocalPlayer == null)
			{
				return;
			}
			LateJoinRecord localRecord = Manager.LocalRecord;
			if (localRecord == null)
			{
				return;
			}
			Player val = ((Manager.Bridge == null) ? null : Manager.Bridge.FindPlayerOwnedByActor(PhotonNetwork.LocalPlayer.ActorNumber));
			if (localRecord.State == LateJoinState.Active)
			{
				if ((Object)(object)val != (Object)null && (Object)(object)val.data != (Object)null && (Object)(object)val.data.input != (Object)null)
				{
					((Behaviour)val.data.input).enabled = true;
				}
				if ((Object)(object)UI != (Object)null)
				{
					UI.Hide();
				}
				return;
			}
			if ((Object)(object)val != (Object)null && (Object)(object)val.data != (Object)null && (Object)(object)val.data.input != (Object)null)
			{
				((Behaviour)val.data.input).enabled = localRecord.State == LateJoinState.CatchUpPicking;
			}
			if (localRecord.State != LateJoinState.CatchUpPicking)
			{
				if ((Object)(object)CardChoiceVisuals.instance != (Object)null)
				{
					CardChoiceVisuals.instance.Hide();
				}
				if ((Object)(object)UI != (Object)null)
				{
					UI.ShowSpectating(localRecord.Status);
				}
			}
		}
	}
}
namespace MidGameJoin.Patches
{
	[HarmonyPatch]
	internal static class DisconnectSafetyPatches
	{
		private static MethodBase TargetMethod()
		{
			Type type = AccessTools.TypeByName("RWF.PrivateRoomHandler");
			if (!(type == null))
			{
				return AccessTools.Method(type, "OnPlayerLeftRoom", (Type[])null, (Type[])null);
			}
			return null;
		}

		private static void Postfix(Player otherPlayer)
		{
			if (otherPlayer != null && !((Object)(object)LateJoinManager.Instance == (Object)null))
			{
				LateJoinManager.Instance.OnRemoteActorLeft(otherPlayer);
			}
		}
	}
	[HarmonyPatch]
	internal static class HostMigrationPatches
	{
		private static MethodBase TargetMethod()
		{
			Type type = AccessTools.TypeByName("RWF.PrivateRoomHandler");
			if (!(type == null))
			{
				return AccessTools.Method(type, "OnMasterClientSwitched", (Type[])null, (Type[])null);
			}
			return null;
		}

		private static void Postfix(Player newMaster)
		{
			if (newMaster != null && !((Object)(object)LateJoinManager.Instance == (Object)null))
			{
				LateJoinManager.Instance.OnRwfHostRestartIssued(newMaster.ActorNumber);
			}
		}
	}
}
namespace MidGameJoin.Core
{
	public sealed class InsertionBatch
	{
		public int Id { get; private set; }

		public int CatchUpCount { get; private set; }

		public IReadOnlyList<int> ActorIds { get; private set; }

		public InsertionBatch(int id, int catchUpCount, IReadOnlyList<int> actorIds)
		{
			Id = id;
			CatchUpCount = catchUpCount;
			ActorIds = actorIds;
		}
	}
	public sealed class LateJoinCoordinator
	{
		private readonly Dictionary<int, LateJoinRecord> records = new Dictionary<int, LateJoinRecord>();

		private int nextBatchId = 1;

		public IReadOnlyList<LateJoinRecord> Records => records.Values.OrderBy((LateJoinRecord r) => r.ActorId).ToArray();

		public LateJoinRecord Register(int actorId, int protocolVersion)
		{
			if (records.TryGetValue(actorId, out var value))
			{
				return value;
			}
			LateJoinRecord lateJoinRecord = new LateJoinRecord(actorId, protocolVersion);
			records.Add(actorId, lateJoinRecord);
			return lateJoinRecord;
		}

		public LateJoinRecord GetRecord(int actorId)
		{
			if (!records.TryGetValue(actorId, out var value))
			{
				return null;
			}
			return value;
		}

		public bool HandshakeAccepted(int actorId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || record.State != LateJoinState.Connecting)
			{
				return false;
			}
			record.State = LateJoinState.Spectating;
			record.Status = "Synchronizing match";
			return true;
		}

		public bool SelectTeam(int actorId, int teamId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || teamId < 0)
			{
				return false;
			}
			if (record.State != LateJoinState.Spectating && record.State != LateJoinState.TeamSelected && record.State != LateJoinState.WaitingForInsertion)
			{
				return false;
			}
			record.SelectedTeamId = teamId;
			if (record.State != LateJoinState.WaitingForInsertion)
			{
				record.State = LateJoinState.TeamSelected;
			}
			return true;
		}

		public bool MarkWaiting(int actorId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null)
			{
				return false;
			}
			if (record.State != LateJoinState.Spectating && record.State != LateJoinState.TeamSelected && record.State != LateJoinState.WaitingForInsertion)
			{
				return false;
			}
			record.State = LateJoinState.WaitingForInsertion;
			record.Status = "Waiting for card picks to finish";
			return true;
		}

		public InsertionBatch LockBatch(IReadOnlyList<int> actorIds, int catchUpCount)
		{
			if (actorIds == null)
			{
				throw new ArgumentNullException("actorIds");
			}
			if (catchUpCount < 0)
			{
				throw new ArgumentOutOfRangeException("catchUpCount");
			}
			List<int> list = new List<int>();
			for (int i = 0; i < actorIds.Count; i++)
			{
				LateJoinRecord record = GetRecord(actorIds[i]);
				if (record != null && record.BatchId == 0 && record.State != LateJoinState.Active && record.State != LateJoinState.Disconnected)
				{
					list.Add(record.ActorId);
				}
			}
			if (list.Count == 0)
			{
				return new InsertionBatch(0, catchUpCount, list.ToArray());
			}
			int num = nextBatchId++;
			foreach (int item in list)
			{
				LateJoinRecord lateJoinRecord = records[item];
				lateJoinRecord.BatchId = num;
				lateJoinRecord.CatchUpCount = catchUpCount;
				lateJoinRecord.CatchUpComplete = catchUpCount == 0;
				lateJoinRecord.State = LateJoinState.WaitingForInsertion;
			}
			return new InsertionBatch(num, catchUpCount, list.ToArray());
		}

		public bool MarkCatchUpComplete(int actorId, int batchId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || batchId <= 0 || record.BatchId != batchId || record.CatchUpComplete)
			{
				return false;
			}
			if (record.State != LateJoinState.CatchUpPicking && record.State != LateJoinState.WaitingForInsertion)
			{
				return false;
			}
			record.CatchUpComplete = true;
			return true;
		}

		public bool Activate(int actorId, int batchId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || record.State == LateJoinState.Active || record.BatchId != batchId)
			{
				return false;
			}
			if (!record.CatchUpComplete && record.CatchUpCount > 0)
			{
				return false;
			}
			record.State = LateJoinState.Active;
			record.Status = string.Empty;
			return true;
		}

		public bool RollBackToSpectator(int actorId, string status)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || record.State == LateJoinState.Disconnected || record.State == LateJoinState.Active)
			{
				return false;
			}
			record.State = LateJoinState.Spectating;
			record.BatchId = 0;
			record.CatchUpCount = 0;
			record.StartingCardCount = 0;
			record.CatchUpComplete = false;
			record.Status = status ?? string.Empty;
			return true;
		}

		public bool TrySelectTeamCommand(int actorId, int commandNonce, int teamId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || commandNonce <= 0 || commandNonce <= record.LastTeamNonce)
			{
				return false;
			}
			if (!SelectTeam(actorId, teamId))
			{
				return false;
			}
			record.LastTeamNonce = commandNonce;
			return true;
		}

		public bool IsBatchResolved(int batchId)
		{
			if (batchId <= 0)
			{
				return true;
			}
			return !records.Values.Any((LateJoinRecord record) => record.BatchId == batchId && record.State != LateJoinState.Active && record.State != LateJoinState.Disconnected);
		}

		public void ClearAndAdvanceBatchSequence(int lastObservedBatchId)
		{
			records.Clear();
			if (lastObservedBatchId >= nextBatchId && lastObservedBatchId < int.MaxValue)
			{
				nextBatchId = lastObservedBatchId + 1;
			}
		}

		public bool ResetPendingForHostMigration(int actorId, string status, bool keepSelectedTeam)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || record.State == LateJoinState.Active || record.State == LateJoinState.Disconnected)
			{
				return false;
			}
			record.State = LateJoinState.Connecting;
			record.BatchId = 0;
			record.CatchUpCount = 0;
			record.StartingCardCount = 0;
			record.CatchUpComplete = false;
			record.SessionReady = false;
			record.ChallengeNonce = 0;
			record.LastTeamNonce = 0;
			if (!keepSelectedTeam)
			{
				record.SelectedTeamId = null;
			}
			record.Status = status ?? string.Empty;
			return true;
		}

		public bool Disconnect(int actorId)
		{
			LateJoinRecord record = GetRecord(actorId);
			if (record == null || record.State == LateJoinState.Disconnected)
			{
				return false;
			}
			record.State = LateJoinState.Disconnected;
			record.BatchId = 0;
			record.CatchUpComplete = true;
			record.Status = "Disconnected";
			return true;
		}
	}
	public sealed class LateJoinRecord
	{
		public int ActorId { get; private set; }

		public int ProtocolVersion { get; set; }

		public LateJoinState State { get; set; }

		public int? SelectedTeamId { get; set; }

		public int BatchId { get; set; }

		public int CatchUpCount { get; set; }

		public int StartingCardCount { get; set; }

		public bool CatchUpComplete { get; set; }

		public string Status { get; set; }

		public int ChallengeNonce { get; set; }

		public int LastTeamNonce { get; set; }

		public bool SessionReady { get; set; }

		public LateJoinRecord(int actorId, int protocolVersion)
		{
			ActorId = actorId;
			ProtocolVersion = protocolVersion;
			State = LateJoinState.Connecting;
			Status = string.Empty;
		}
	}
	public static class LateJoinRules
	{
		public static int CalculateCatchUpCount(IReadOnlyList<int> cardCounts)
		{
			if (cardCounts == null || cardCounts.Count == 0)
			{
				return 0;
			}
			long num = 0L;
			for (int i = 0; i < cardCounts.Count; i++)
			{
				if (cardCounts[i] < 0)
				{
					throw new ArgumentOutOfRangeException("cardCounts", "Card counts cannot be negative.");
				}
				num += cardCounts[i];
			}
			return (int)(num / cardCounts.Count);
		}
	}
	public enum LateJoinState
	{
		Connecting,
		Spectating,
		TeamSelected,
		WaitingForInsertion,
		CatchUpPicking,
		Active,
		Disconnected
	}
	public static class ProtocolInfo
	{
		public const int ProtocolMajor = 1;

		public const int ProtocolVersion = 100;

		public const string PluginGuid = "com.thunderstore.midgamejoin";

		public const string PluginName = "MidGameJoin";

		public const string PluginVersion = "0.1.3";
	}
	public sealed class SessionSnapshot
	{
		public string GameModeId { get; set; }

		public int CurrentLevelId { get; set; }

		public bool BattleOngoing { get; set; }

		public bool IsTransitioning { get; set; }

		public int PickEndGeneration { get; set; }

		public int ActivePlayerCount { get; set; }

		public int[] TeamIds { get; set; }

		public int[] TeamPoints { get; set; }

		public int[] TeamRounds { get; set; }
	}
	public static class SessionSyncRules
	{
		public static bool HasAllActivePlayers(int actualPlayers, int expectedPlayers)
		{
			if (actualPlayers < 0)
			{
				actualPlayers = 0;
			}
			if (expectedPlayers < 0)
			{
				expectedPlayers = 0;
			}
			return actualPlayers >= expectedPlayers;
		}
	}
	public sealed class EncodedRecords
	{
		public int[] ActorIds { get; set; }

		public int[] States { get; set; }

		public int[] TeamIds { get; set; }

		public int[] BatchIds { get; set; }

		public int[] CatchUpCounts { get; set; }

		public int[] ProtocolVersions { get; set; }

		public string[] Statuses { get; set; }
	}
	public static class SnapshotCodec
	{
		public static EncodedRecords EncodeRecords(IReadOnlyList<LateJoinRecord> records)
		{
			if (records == null)
			{
				throw new ArgumentNullException("records");
			}
			EncodedRecords encodedRecords = new EncodedRecords
			{
				ActorIds = new int[records.Count],
				States = new int[records.Count],
				TeamIds = new int[records.Count],
				BatchIds = new int[records.Count],
				CatchUpCounts = new int[records.Count],
				ProtocolVersions = new int[records.Count],
				Statuses = new string[records.Count]
			};
			for (int i = 0; i < records.Count; i++)
			{
				LateJoinRecord lateJoinRecord = records[i];
				encodedRecords.ActorIds[i] = lateJoinRecord.ActorId;
				encodedRecords.States[i] = (int)lateJoinRecord.State;
				encodedRecords.TeamIds[i] = (lateJoinRecord.SelectedTeamId.HasValue ? lateJoinRecord.SelectedTeamId.Value : (-1));
				encodedRecords.BatchIds[i] = lateJoinRecord.BatchId;
				encodedRecords.CatchUpCounts[i] = lateJoinRecord.CatchUpCount;
				encodedRecords.ProtocolVersions[i] = lateJoinRecord.ProtocolVersion;
				encodedRecords.Statuses[i] = lateJoinRecord.Status ?? string.Empty;
			}
			return encodedRecords;
		}

		public static LateJoinRecord[] DecodeRecords(int[] actorIds, int[] states, int[] teamIds, int[] batchIds, int[] catchUpCounts, int[] protocolVersions, string[] statuses)
		{
			if (actorIds == null || states == null || teamIds == null || batchIds == null || catchUpCounts == null || protocolVersions == null || statuses == null)
			{
				throw new ArgumentNullException("Snapshot record arrays cannot be null.");
			}
			int num = actorIds.Length;
			if (states.Length != num || teamIds.Length != num || batchIds.Length != num || catchUpCounts.Length != num || protocolVersions.Length != num || statuses.Length != num)
			{
				throw new ArgumentException("Snapshot record arrays must have identical lengths.");
			}
			LateJoinRecord[] array = new LateJoinRecord[num];
			for (int i = 0; i < num; i++)
			{
				if (!Enum.IsDefined(typeof(LateJoinState), states[i]))
				{
					throw new ArgumentException("Invalid late join state value.");
				}
				LateJoinRecord lateJoinRecord = new LateJoinRecord(actorIds[i], protocolVersions[i])
				{
					State = (LateJoinState)states[i],
					SelectedTeamId = ((teamIds[i] < 0) ? ((int?)null) : new int?(teamIds[i])),
					BatchId = batchIds[i],
					CatchUpCount = catchUpCounts[i],
					Status = (statuses[i] ?? string.Empty)
				};
				array[i] = lateJoinRecord;
			}
			return array;
		}
	}
}