Decompiled source of ScoreRace v4.1.0

ScoreRace.dll

Decompiled 4 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Photon.Pun;
using Photon.Realtime;
using Photon.Voice;
using Photon.Voice.Unity;
using REPOJP.ScoreRace.Core;
using Steamworks;
using Steamworks.Data;
using TMPro;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("REPOJP")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("4.1.0.0")]
[assembly: AssemblyInformationalVersion("4.1.0")]
[assembly: AssemblyProduct("ScoreRace")]
[assembly: AssemblyTitle("ScoreRace")]
[assembly: AssemblyVersion("4.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 REPOJP.ScoreRace
{
	internal sealed class CosmeticTracker
	{
		internal sealed class Pending
		{
			public int Before;

			public int Rarity;

			public CosmeticDelivery Delivery;
		}

		private readonly Dictionary<int, CosmeticBox> boxes = new Dictionary<int, CosmeticBox>();

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

		public CosmeticBox[] Initial => boxes.Values.OrderBy<CosmeticBox, string>((CosmeticBox b) => b.Id, StringComparer.Ordinal).ToArray();

		public CosmeticDelivery[] Delivered => delivered.Values.OrderBy<CosmeticDelivery, string>((CosmeticDelivery b) => b.Id, StringComparer.Ordinal).ToArray();

		public int Count => delivered.Count;

		public int ReferenceWeight => Cosmetics.ReferenceWeight(Initial);

		public double Bonus => Cosmetics.Bonus(Delivered);

		private static List<Rarity> Extracted => Access.Read<List<Rarity>>(RoundDirector.instance, "cosmeticWorldObjectsExtracted");

		public void Restore(CosmeticDelivery[] values)
		{
			if (!Cosmetics.Matches(values, Initial))
			{
				throw new InvalidOperationException("Invalid team cosmetic checkpoint.");
			}
			delivered.Clear();
			foreach (CosmeticDelivery cosmeticDelivery in values)
			{
				delivered[cosmeticDelivery.Id] = cosmeticDelivery;
			}
		}

		public void Clear()
		{
			boxes.Clear();
			delivered.Clear();
		}

		public IEnumerable<string> Capture()
		{
			Clear();
			CosmeticWorldObject[] array = Object.FindObjectsOfType<CosmeticWorldObject>();
			foreach (CosmeticWorldObject val in array)
			{
				NotValuableObject component = ((Component)val).GetComponent<NotValuableObject>();
				if ((Object)(object)component == (Object)null || !component.hasHealth || component.healthMax <= 0)
				{
					throw new InvalidOperationException("A cosmetic box has no valid health data.");
				}
				Cosmetics.RarityWeight((int)val.rarity);
				int num = Access.Read<int>(component, "healthCurrent");
				if (num < 0 || num > component.healthMax)
				{
					throw new InvalidOperationException("Invalid initial cosmetic box health.");
				}
				string text = GameBridge.Pose(((Component)val).transform);
				string id = Hash.Text("cosmetic:" + text);
				if (boxes.Values.Any((CosmeticBox b) => b.Id == id))
				{
					throw new InvalidOperationException("Cosmetic box positions are ambiguous.");
				}
				if (boxes.Count >= 1024)
				{
					throw new InvalidOperationException("Too many cosmetic boxes.");
				}
				boxes.Add(TeamRuntime.ObjectId((Component)(object)val), new CosmeticBox
				{
					Id = id,
					Rarity = (int)val.rarity,
					MaximumHealth = component.healthMax,
					InitialHealth = num
				});
				yield return "cosmetic:" + text + ":" + ((int)val.rarity).ToString(CultureInfo.InvariantCulture) + ":" + num.ToString(CultureInfo.InvariantCulture) + "/" + component.healthMax.ToString(CultureInfo.InvariantCulture);
			}
		}

		public Pending? BeforeExtract(CosmeticWorldObject box)
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Expected I4, but got Unknown
			//IL_0066: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Expected I4, but got Unknown
			if (!boxes.TryGetValue(TeamRuntime.ObjectId((Component)(object)box), out CosmeticBox value) || delivered.ContainsKey(value.Id))
			{
				return null;
			}
			NotValuableObject component = ((Component)box).GetComponent<NotValuableObject>();
			return new Pending
			{
				Before = Extracted.Count,
				Rarity = (int)box.rarity,
				Delivery = new CosmeticDelivery
				{
					Id = value.Id,
					Rarity = (int)box.rarity,
					Health = Math.Max(0, Access.Read<int>(component, "healthCurrent")),
					MaximumHealth = component.healthMax
				}
			};
		}

		public void AfterExtract(Pending? pending)
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Invalid comparison between Unknown and I4
			if (pending != null && Extracted.Count == pending.Before + 1 && (int)Extracted[Extracted.Count - 1] == pending.Rarity)
			{
				if (!Cosmetics.Matches(new CosmeticDelivery[1] { pending.Delivery }, Initial))
				{
					throw new InvalidOperationException("Invalid cosmetic box extraction health.");
				}
				if (!delivered.ContainsKey(pending.Delivery.Id))
				{
					delivered.Add(pending.Delivery.Id, pending.Delivery);
				}
			}
		}
	}
	internal sealed class DeadVoice : IDisposable
	{
		private sealed class Playback : IDisposable
		{
			private readonly Queue<float> queue = new Queue<float>();

			private readonly object gate = new object();

			public readonly AudioSource Source;

			public int Sequence = -1;

			public double At;

			private readonly AudioClip clip;

			public Playback(string id)
			{
				//IL_002e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0034: Expected O, but got Unknown
				//IL_008b: Unknown result type (might be due to invalid IL or missing references)
				//IL_0095: Expected O, but got Unknown
				GameObject val = new GameObject("ScoreRace Dead Voice " + id);
				Object.DontDestroyOnLoad((Object)(object)val);
				Source = val.AddComponent<AudioSource>();
				Source.spatialBlend = 0f;
				Source.loop = true;
				Source.volume = 0.5f;
				clip = AudioClip.Create("ScoreRace dead voice", 8000, 1, 8000, true, new PCMReaderCallback(Read));
				Source.clip = clip;
				Source.Play();
			}

			private void Read(float[] data)
			{
				lock (gate)
				{
					for (int i = 0; i < data.Length; i++)
					{
						data[i] = ((queue.Count > 0) ? queue.Dequeue() : 0f);
					}
				}
			}

			public void Add(byte[] data)
			{
				lock (gate)
				{
					if (queue.Count > 1600)
					{
						queue.Clear();
					}
					for (int i = 25; i < data.Length; i++)
					{
						queue.Enqueue(MediaPackets.MuLaw(data[i]));
					}
				}
			}

			public void Dispose()
			{
				Source.Stop();
				Object.Destroy((Object)(object)((Component)Source).gameObject);
				Object.Destroy((Object)(object)clip);
				lock (gate)
				{
					queue.Clear();
				}
			}
		}

		private const int Channel = 95;

		private const int Rate = 8000;

		private const int Samples = 160;

		private readonly Plugin p;

		private readonly object captureLock = new object();

		private readonly Queue<byte[]> outgoing = new Queue<byte[]>();

		private readonly List<byte> frame = new List<byte>(160);

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

		private readonly Dictionary<AudioSource, bool> muted = new Dictionary<AudioSource, bool>();

		private volatile object? sourceVoice;

		private volatile bool blockNative;

		private volatile bool capture;

		private volatile int sourceRate = 24000;

		private volatile int sourceChannels = 1;

		private volatile float gain = 1f;

		private double resample;

		private int sequence;

		private volatile int captureEpoch;

		private volatile int captureChannel;

		private DeadVoiceChannel selectedChannel = DeadVoiceChannel.Global;

		private string channelRound = "";

		private RectTransform? channelRoot;

		private SimpleButton? channelButton;

		private string round = "";

		private string policy = "";

		private string micDevice = "";

		private byte[] roundBytes = Array.Empty<byte>();

		private AudioClip? mic;

		private int micPosition;

		public DeadVoiceChannel EffectiveChannel => VoiceAudience.Effective(p.Race.Observing ? (-1) : (p.Race.Plan?.Team(p.Race.Net.Self)?.Number).GetValueOrDefault(), p.Race.LocalEliminated, (!(channelRound == p.Race.Plan?.Id)) ? DeadVoiceChannel.Global : selectedChannel);

		private bool CanSwitch
		{
			get
			{
				if (p.Game.Active && p.Race.Multiplayer && p.Race.Plan != null && !p.Race.Observing && p.Race.Plan.Team(p.Race.Net.Self) != null && p.Race.LocalDead && p.Race.LocalEliminated && p.Race.State >= Phase.Countdown)
				{
					return p.Race.State <= Phase.Finished;
				}
				return false;
			}
		}

		public DeadVoice(Plugin plugin)
		{
			p = plugin;
		}

		private void ClearAudio()
		{
			capture = false;
			lock (captureLock)
			{
				captureEpoch++;
				outgoing.Clear();
				frame.Clear();
				resample = 0.0;
			}
			foreach (Playback value in speakers.Values)
			{
				value.Dispose();
			}
			speakers.Clear();
		}

		private void SwitchChannel()
		{
			if (CanSwitch)
			{
				ClearAudio();
				selectedChannel = ((EffectiveChannel != DeadVoiceChannel.Global) ? DeadVoiceChannel.Global : DeadVoiceChannel.Team);
				channelRound = p.Race.Plan.Id;
				policy = "";
			}
		}

		public void Render()
		{
			//IL_00a0: Unknown result type (might be due to invalid IL or missing references)
			if (!CanSwitch || ResultButtons.NativeMenuOpen || ((Object)(object)LoadingUI.instance != (Object)null && ((Component)LoadingUI.instance).gameObject.activeInHierarchy))
			{
				if ((Object)(object)channelRoot != (Object)null)
				{
					((Component)channelRoot).gameObject.SetActive(false);
				}
				return;
			}
			if ((Object)(object)channelRoot == (Object)null)
			{
				BigMessageUI instance = BigMessageUI.instance;
				TextMeshProUGUI val = ((instance != null) ? ((Component)instance).GetComponent<TextMeshProUGUI>() : null);
				if ((Object)(object)val == (Object)null)
				{
					return;
				}
				channelRoot = new GameObject("ScoreRace Voice Channel", new Type[2]
				{
					typeof(RectTransform),
					typeof(Canvas)
				}).GetComponent<RectTransform>();
				Object.DontDestroyOnLoad((Object)(object)((Component)channelRoot).gameObject);
				Canvas component = ((Component)channelRoot).GetComponent<Canvas>();
				component.renderMode = (RenderMode)0;
				component.sortingOrder = 29000;
				channelButton = new SimpleButton(channelRoot, val, "");
			}
			((Component)channelRoot).gameObject.SetActive(true);
			float scale = Mathf.Min((float)Screen.width / 1280f, (float)Screen.height / 720f);
			channelButton.Place(423f, 343f, 400f, 30f, 22f, scale);
			((TMP_Text)channelButton.Label).text = "VOICE: " + ((EffectiveChannel == DeadVoiceChannel.Team) ? "TEAM" : "GLOBAL") + " [F2]";
			if (channelButton.Draw())
			{
				SwitchChannel();
			}
		}

		public unsafe void Tick()
		{
			//IL_04c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_04c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_04df: Unknown result type (might be due to invalid IL or missing references)
			//IL_046a: Unknown result type (might be due to invalid IL or missing references)
			string text = p.Race.Plan?.Id ?? "";
			if (channelRound != text)
			{
				channelRound = text;
				selectedChannel = DeadVoiceChannel.Global;
			}
			if (CanSwitch && Application.isFocused && !ResultButtons.NativeMenuOpen && Input.GetKeyDown((KeyCode)283))
			{
				SwitchChannel();
			}
			bool flag = p.Game.Active && p.Race.Multiplayer && p.Race.Plan != null && p.Race.State >= Phase.Countdown && p.Race.State <= Phase.Finished;
			bool dead = flag && p.Race.LocalDead;
			blockNative = dead;
			PlayerVoiceChat instance = PlayerVoiceChat.instance;
			Recorder val = (((Object)(object)instance != (Object)null) ? Access.Read<Recorder>(instance, "recorder") : null);
			sourceVoice = (((Object)(object)val != (Object)null) ? Access.Read<LocalVoice>(val, "voice") : null);
			if ((Object)(object)val != (Object)null)
			{
				IAudioDesc val2 = Access.Read<IAudioDesc>(val, "inputSource");
				if (val2 != null)
				{
					sourceRate = val2.SamplingRate;
					sourceChannels = Math.Max(1, val2.Channels);
				}
			}
			gain = (((Object)(object)instance != (Object)null) ? Mathf.Clamp(Access.Read<float>(instance, "microphoneVolumeMultiplier"), 0f, 4f) : 1f);
			string[] array = (flag ? p.Race.Participants.Where((string id) => id != p.Race.Net.Self && p.Spectators.CanTalk(id)).ToArray() : Array.Empty<string>());
			string text2 = (flag ? p.Race.Plan.Id : "");
			string text3 = text2 + ":" + dead + ":" + p.Race.LocalEliminated + ":" + EffectiveChannel.ToString() + ":" + string.Join("|", array);
			if (policy != text3)
			{
				ClearAudio();
				policy = text3;
			}
			if (round != text2)
			{
				round = text2;
				roundBytes = ((round.Length == 32) ? new Guid(round).ToByteArray() : Array.Empty<byte>());
				sequence = 0;
			}
			captureChannel = (int)EffectiveChannel;
			bool flag2 = (Object)(object)instance == (Object)null || Access.Read<bool>(instance, "microphoneEnabled");
			capture = dead && array.Length != 0 && flag2 && (Object)(object)DataDirector.instance != (Object)null && !Access.Read<bool>(DataDirector.instance, "toggleMute") && ((Object)(object)AudioManager.instance == (Object)null || !Access.Read<bool>(AudioManager.instance, "pushToTalk") || SemiFunc.InputHold((InputKey)25));
			if (dead && sourceVoice == null)
			{
				FallbackMicrophone();
			}
			else
			{
				StopMicrophone();
			}
			if (flag)
			{
				PlayerVoiceChat[] array2 = Object.FindObjectsOfType<PlayerVoiceChat>();
				foreach (PlayerVoiceChat voice in array2)
				{
					GateNative(voice);
				}
			}
			else
			{
				RestoreMute();
			}
			if (!SteamClient.IsValid)
			{
				return;
			}
			for (int num2 = 0; num2 < 8; num2++)
			{
				byte[] array3;
				lock (captureLock)
				{
					array3 = ((outgoing.Count > 0) ? outgoing.Dequeue() : null);
				}
				if (array3 == null)
				{
					break;
				}
				if (!capture)
				{
					continue;
				}
				foreach (string item in array.Where(p.Spectators.CanTalk))
				{
					SteamNetworking.SendP2PPacket(SteamId.op_Implicit(ulong.Parse(item)), array3, array3.Length, 95, (P2PSend)0);
				}
			}
			for (int num3 = 0; num3 < 256; num3++)
			{
				if (!SteamNetworking.IsP2PPacketAvailable(95))
				{
					break;
				}
				P2Packet? val3 = SteamNetworking.ReadP2PPacket(95);
				if (!val3.HasValue)
				{
					break;
				}
				P2Packet value = val3.Value;
				string text4 = ((object)(*(SteamId*)(&value.SteamId))/*cast due to .constrained prefix*/).ToString();
				byte[] data = val3.Value.Data;
				if (!dead || !p.Spectators.CanTalk(text4) || data.Length != 185)
				{
					continue;
				}
				using BinaryReader binaryReader = new BinaryReader(new MemoryStream(data));
				if (binaryReader.ReadInt32() != 1397899576 || new Guid(binaryReader.ReadBytes(16)).ToString("N") != round)
				{
					continue;
				}
				int num4 = binaryReader.ReadInt32();
				DeadVoiceChannel deadVoiceChannel = (DeadVoiceChannel)binaryReader.ReadByte();
				if (num4 < 0 || deadVoiceChannel != EffectiveChannel || deadVoiceChannel != p.Spectators.Presence(text4)?.VoiceChannel)
				{
					continue;
				}
				if (!speakers.TryGetValue(text4, out Playback value2))
				{
					value2 = (speakers[text4] = new Playback(text4));
				}
				if (num4 > value2.Sequence)
				{
					value2.Sequence = num4;
					value2.At = Transport.Now;
					value2.Add(data);
					if ((Object)(object)instance != (Object)null)
					{
						value2.Source.outputAudioMixerGroup = instance.mixerMicrophoneSpectate;
					}
				}
			}
			string[] array4 = speakers.Keys.Where((string id) => !dead || !p.Spectators.CanTalk(id) || Transport.Now - speakers[id].At > 3.0).ToArray();
			foreach (string key in array4)
			{
				speakers[key].Dispose();
				speakers.Remove(key);
			}
		}

		public bool Input(object voice, float[] samples)
		{
			if (voice != sourceVoice || !blockNative)
			{
				return true;
			}
			int num = captureEpoch;
			if (capture)
			{
				lock (captureLock)
				{
					if (num == captureEpoch)
					{
						Feed(samples, sourceRate, sourceChannels);
					}
				}
			}
			return false;
		}

		public bool Input(object voice, short[] samples)
		{
			if (voice != sourceVoice || !blockNative)
			{
				return true;
			}
			if (capture)
			{
				int num = captureEpoch;
				float[] array = new float[samples.Length];
				for (int i = 0; i < samples.Length; i++)
				{
					array[i] = (float)samples[i] / 32768f;
				}
				lock (captureLock)
				{
					if (num == captureEpoch)
					{
						Feed(array, sourceRate, sourceChannels);
					}
				}
			}
			return false;
		}

		private void Feed(float[] samples, int rate, int channels)
		{
			if (!capture || roundBytes.Length != 16 || rate < 8000 || rate > 192000 || channels < 1 || channels > 8)
			{
				return;
			}
			for (int i = 0; i + channels <= samples.Length; i += channels)
			{
				resample += 8000.0;
				if (resample < (double)rate)
				{
					continue;
				}
				resample -= rate;
				float num = 0f;
				for (int j = 0; j < channels; j++)
				{
					num += samples[i + j];
				}
				frame.Add(MediaPackets.MuLaw(num / (float)channels * gain));
				if (frame.Count != 160)
				{
					continue;
				}
				using MemoryStream memoryStream = new MemoryStream();
				using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
				binaryWriter.Write(1397899576);
				binaryWriter.Write(roundBytes);
				binaryWriter.Write(sequence++);
				binaryWriter.Write((byte)captureChannel);
				binaryWriter.Write(frame.ToArray());
				frame.Clear();
				if (outgoing.Count >= 10)
				{
					outgoing.Dequeue();
				}
				outgoing.Enqueue(memoryStream.ToArray());
			}
		}

		private void FallbackMicrophone()
		{
			string text = (((Object)(object)SessionManager.instance != (Object)null) ? Access.Read<string>(SessionManager.instance, "micDeviceCurrent") : "");
			if (!capture)
			{
				StopMicrophone();
				return;
			}
			if ((Object)(object)mic == (Object)null || text != micDevice)
			{
				StopMicrophone();
				if (!string.IsNullOrEmpty(text) && Microphone.devices.Contains(text))
				{
					micDevice = text;
					mic = Microphone.Start(string.IsNullOrEmpty(text) ? null : text, true, 1, 8000);
					micPosition = 0;
				}
				return;
			}
			int position = Microphone.GetPosition(string.IsNullOrEmpty(micDevice) ? null : micDevice);
			if (position < 0)
			{
				return;
			}
			int num = (position - micPosition + mic.samples) % mic.samples;
			while (num >= 160)
			{
				float[] array = new float[160 * mic.channels];
				mic.GetData(array, micPosition);
				micPosition = (micPosition + 160) % mic.samples;
				num -= 160;
				lock (captureLock)
				{
					Feed(array, mic.frequency, mic.channels);
				}
			}
		}

		private void StopMicrophone()
		{
			if ((Object)(object)mic != (Object)null)
			{
				Microphone.End(string.IsNullOrEmpty(micDevice) ? null : micDevice);
				Object.Destroy((Object)(object)mic);
				mic = null;
				micPosition = 0;
			}
		}

		public void GateNative(PlayerVoiceChat voice)
		{
			if (!p.Game.Active || !p.Race.Multiplayer)
			{
				return;
			}
			PlayerAvatar val = Access.Read<PlayerAvatar>(voice, "playerAvatar");
			if ((Object)(object)val == (Object)null)
			{
				return;
			}
			bool localDead = p.Race.LocalDead;
			bool flag = Access.Read<bool>(val, "deadSet");
			if ((Object)(object)val == (Object)(object)PlayerAvatar.instance && localDead)
			{
				Recorder val2 = Access.Read<Recorder>(voice, "recorder");
				if ((Object)(object)val2 != (Object)null)
				{
					val2.TransmitEnabled = false;
				}
				Access.Set(voice, "isTalking", false);
			}
			bool flag2 = localDead || flag;
			AudioSource[] array = (AudioSource[])(object)new AudioSource[2]
			{
				Access.Read<AudioSource>(voice, "audioSource"),
				voice.ttsAudioSource
			};
			foreach (AudioSource val3 in array)
			{
				if ((Object)(object)val3 == (Object)null)
				{
					continue;
				}
				bool value;
				if (flag2)
				{
					if (!muted.ContainsKey(val3))
					{
						muted[val3] = val3.mute;
					}
					val3.mute = true;
				}
				else if (muted.TryGetValue(val3, out value))
				{
					val3.mute = value;
					muted.Remove(val3);
				}
			}
		}

		private void RestoreMute()
		{
			foreach (KeyValuePair<AudioSource, bool> item in muted)
			{
				if ((Object)(object)item.Key != (Object)null)
				{
					item.Key.mute = item.Value;
				}
			}
			muted.Clear();
		}

		public void Dispose()
		{
			if ((Object)(object)channelRoot != (Object)null)
			{
				Object.Destroy((Object)(object)((Component)channelRoot).gameObject);
			}
			channelRoot = null;
			capture = (blockNative = false);
			sourceVoice = null;
			StopMicrophone();
			RestoreMute();
			foreach (Playback value in speakers.Values)
			{
				value.Dispose();
			}
			speakers.Clear();
			lock (captureLock)
			{
				frame.Clear();
				outgoing.Clear();
			}
		}
	}
	internal sealed class DeliveryTracker
	{
		internal sealed class AbsorbStart
		{
			public object[] Before = Array.Empty<object>();

			public int Source;
		}

		internal sealed class BoxUpdate
		{
			public object[] Before = Array.Empty<object>();

			public object[] Ready = Array.Empty<object>();

			public float Value;
		}

		public readonly DeliveryLedger Ledger = new DeliveryLedger();

		public int ExtractionDepth;

		public int TaxReturnDepth;

		private readonly Dictionary<object, int> absorbedSources = new Dictionary<object, int>();

		public void Clear()
		{
			Ledger.Clear();
			absorbedSources.Clear();
			ExtractionDepth = (TaxReturnDepth = 0);
		}

		public void Capture(IEnumerable<ValuableObject> valuables)
		{
			Clear();
			foreach (ValuableObject valuable in valuables)
			{
				Ledger.Register(TeamRuntime.ObjectId((Component)(object)valuable), (long)Access.Read<float>(valuable, "dollarValueOriginal"));
			}
		}

		public int Source(ValuableObject v)
		{
			int num = TeamRuntime.ObjectId((Component)(object)v);
			if (!Ledger.Known(num))
			{
				if (!Access.Read<bool>(v, "dollarValueSet"))
				{
					throw new InvalidOperationException("Delivered valuable price is not ready.");
				}
				Ledger.Register(num, (long)Access.Read<float>(v, "dollarValueOriginal"));
			}
			return num;
		}

		private static object[] Absorptions(ItemValuableBox box)
		{
			return Access.Read<IList>(box, "activeAbsorptions").Cast<object>().ToArray();
		}

		public AbsorbStart BeforeAbsorb(ItemValuableBox box, PhysGrabObject target)
		{
			ValuableObject component = ((Component)target).GetComponent<ValuableObject>();
			return new AbsorbStart
			{
				Before = Absorptions(box),
				Source = (((Object)(object)component != (Object)null) ? Source(component) : 0)
			};
		}

		public void AfterAbsorb(ItemValuableBox box, AbsorbStart state)
		{
			foreach (object item in Absorptions(box).Except(state.Before))
			{
				absorbedSources[item] = state.Source;
			}
		}

		public BoxUpdate BeforeUpdate(ItemValuableBox box)
		{
			object[] array = Absorptions(box);
			return new BoxUpdate
			{
				Before = array,
				Value = box.CurrentValue,
				Ready = (from entry in array.Reverse()
					where (Object)(object)Access.Read<GameObject>(entry, "meshCopy") != (Object)null && Mathf.Clamp01(Access.Read<float>(entry, "timer") / (Access.Read<float>(entry, "effectiveDuration") * 2.2f)) >= 1f
					select entry).ToArray()
			};
		}

		public void AfterUpdate(ItemValuableBox box, BoxUpdate state)
		{
			object[] second = Absorptions(box);
			object[] array = state.Ready.Except(second).ToArray();
			float num = state.Value;
			object[] array2 = array;
			foreach (object o in array2)
			{
				num = Mathf.Min(num + Access.Read<float>(o, "valueToAdd"), box.valueLimit);
			}
			if (Math.Abs(num - box.CurrentValue) > 0.05f)
			{
				throw new InvalidOperationException("Valuable box accounting changed during absorption.");
			}
			float num2 = state.Value;
			array2 = array;
			foreach (object obj in array2)
			{
				float num3 = Mathf.Min(num2 + Access.Read<float>(obj, "valueToAdd"), box.valueLimit);
				if (absorbedSources.TryGetValue(obj, out var value))
				{
					Ledger.Store(TeamRuntime.ObjectId((Component)(object)box), value, (long)Math.Max(0f, num3 - num2));
				}
				num2 = num3;
			}
			foreach (object item in state.Before.Except(second))
			{
				absorbedSources.Remove(item);
			}
		}
	}
	internal static class Determinism
	{
		private sealed class Stream : IEnumerator, IDisposable
		{
			private readonly IEnumerator inner;

			private State state;

			private readonly bool pause;

			private bool held;

			public object? Current
			{
				get
				{
					if (!held)
					{
						return inner.Current;
					}
					return null;
				}
			}

			public Stream(IEnumerator inner, int seed, bool pause)
			{
				//IL_0014: Unknown result type (might be due to invalid IL or missing references)
				//IL_0020: Unknown result type (might be due to invalid IL or missing references)
				//IL_0025: Unknown result type (might be due to invalid IL or missing references)
				this.inner = inner;
				this.pause = pause;
				State val = Random.state;
				Random.InitState(seed);
				state = Random.state;
				Random.state = val;
			}

			public bool MoveNext()
			{
				//IL_0078: Unknown result type (might be due to invalid IL or missing references)
				//IL_007d: 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_0058: Unknown result type (might be due to invalid IL or missing references)
				//IL_005d: Unknown result type (might be due to invalid IL or missing references)
				//IL_005f: Unknown result type (might be due to invalid IL or missing references)
				held = pause && Plugin.Instance.Game.Active && Plugin.Instance.Game.Frozen;
				if (held)
				{
					return true;
				}
				if (!Plugin.Instance.Game.Active)
				{
					return inner.MoveNext();
				}
				State val = Random.state;
				Random.state = state;
				try
				{
					return inner.MoveNext();
				}
				finally
				{
					state = Random.state;
					Random.state = val;
				}
			}

			public void Reset()
			{
				throw new NotSupportedException();
			}

			public void Dispose()
			{
				if (inner is IDisposable disposable)
				{
					disposable.Dispose();
				}
			}
		}

		private static readonly Dictionary<string, int> calls = new Dictionary<string, int>();

		public static bool Enabled
		{
			get
			{
				Plugin instance = Plugin.Instance;
				if (instance != null && instance.Game.Active)
				{
					Race race = Plugin.Instance.Race;
					if (race == null)
					{
						return false;
					}
					return race.State == Phase.Generating;
				}
				return false;
			}
		}

		public static void Reset()
		{
			calls.Clear();
		}

		private static int Seed(object? instance, MethodBase method)
		{
			string text = method.DeclaringType.Name + "." + method.Name;
			Component val = (Component)((instance is Component) ? instance : null);
			if (val != null)
			{
				text = text + "/" + GameBridge.Pose(val.transform);
			}
			calls.TryGetValue(text, out var value);
			calls[text] = value + 1;
			return Hash.Seed(Plugin.Instance.Game.Plan.Seed + "/" + text + "/" + value);
		}

		public static void Enter(object? __instance, MethodBase __originalMethod, out State? __state)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			__state = null;
			if (Enabled)
			{
				__state = Random.state;
				Random.InitState(Seed(__instance, __originalMethod));
			}
		}

		public static Exception? Leave(Exception? __exception, State? __state)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			if (__state.HasValue)
			{
				Random.state = __state.Value;
			}
			return __exception;
		}

		public static void Wrap(object? __instance, MethodBase __originalMethod, ref IEnumerator __result)
		{
			if (Enabled && __result != null)
			{
				__result = new Stream(__result, Seed(__instance, __originalMethod), __originalMethod.DeclaringType == typeof(EnemyParent) && (__originalMethod.Name == "Logic" || __originalMethod.Name == "PlayerCloseLogic"));
			}
		}

		public static void Install(Harmony h)
		{
			//IL_0123: Unknown result type (might be due to invalid IL or missing references)
			//IL_0130: Expected O, but got Unknown
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0206: Expected O, but got Unknown
			//IL_0206: Expected O, but got Unknown
			string[] array = new string[17]
			{
				"LevelGenerator", "ValuableDirector", "ValuableObject", "Module", "ModulePropSwitch", "ModulePropLight", "ModuleProp", "EnemyDirector", "EnemyParent", "Enemy",
				"RoundDirector", "ExtractionPoint", "LevelPoint", "LevelPointEnemy", "LevelPointNavMesh", "PhysGrabObject", "ItemAttributes"
			};
			foreach (string name in array)
			{
				Type type = typeof(LevelGenerator).Assembly.GetType(name, throwOnError: false);
				if (type == null)
				{
					continue;
				}
				MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				foreach (MethodInfo methodInfo in methods)
				{
					if (!methodInfo.IsGenericMethod && !methodInfo.IsAbstract)
					{
						if (methodInfo.ReturnType == typeof(IEnumerator))
						{
							h.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(Determinism), "Wrap", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
						}
						else if (methodInfo.Name == "Start" || methodInfo.Name == "Awake" || methodInfo.Name.Contains("Setup") || methodInfo.Name.Contains("Generation") || methodInfo.Name.Contains("Spawn") || methodInfo.Name == "DollarValueSetLogic" || methodInfo.Name == "GetEnemy" || methodInfo.Name == "GetValue")
						{
							h.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Determinism), "Enter", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Determinism), "Leave", (Type[])null), (HarmonyMethod)null);
						}
					}
				}
			}
		}
	}
	internal sealed class EnvironmentScan
	{
		private readonly Dictionary<string, (long length, long ticks, string hash)> cache = new Dictionary<string, (long, long, string)>(StringComparer.OrdinalIgnoreCase);

		private readonly string ownConfig;

		public EnvironmentScan(string ownConfigPath)
		{
			ownConfig = Path.GetFullPath(ownConfigPath);
		}

		public Task<Manifest> Capture(bool force)
		{
			List<ManifestEntry> entries = new List<ManifestEntry>();
			List<string> errors = new List<string>();
			Dictionary<string, SortedDictionary<string, string>> configs = new Dictionary<string, SortedDictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
			Add("runtime", "game", Application.version, Application.version);
			Add("runtime", "loader", typeof(BaseUnityPlugin).Assembly.GetName().Version.ToString(), typeof(BaseUnityPlugin).Assembly.GetName().Version.ToString());
			foreach (PluginInfo item in Chainloader.PluginInfos.Values.OrderBy<PluginInfo, string>((PluginInfo x) => x.Metadata.GUID, StringComparer.Ordinal))
			{
				Add("mod", item.Metadata.GUID, item.Metadata.Version.ToString(), item.Metadata.Name + " " + item.Metadata.Version);
				if ((Object)(object)item.Instance == (Object)null)
				{
					errors.Add("Cannot inspect plugin: " + item.Metadata.GUID);
					continue;
				}
				string fullPath = Path.GetFullPath(item.Instance.Config.ConfigFilePath);
				if (Same(fullPath, ownConfig))
				{
					continue;
				}
				if (!configs.TryGetValue(fullPath, out SortedDictionary<string, string> value))
				{
					value = (configs[fullPath] = new SortedDictionary<string, string>(StringComparer.Ordinal));
				}
				foreach (KeyValuePair<ConfigDefinition, ConfigEntryBase> item2 in item.Instance.Config)
				{
					value[item2.Key.Section + "\0" + item2.Key.Key] = item2.Value.GetSerializedValue();
				}
			}
			return Task.Run(delegate
			{
				try
				{
					HashSet<string> hashSet = new HashSet<string>(configs.Keys, StringComparer.OrdinalIgnoreCase);
					if (Directory.Exists(Paths.ConfigPath))
					{
						foreach (string item3 in Directory.EnumerateFiles(Paths.ConfigPath, "*", SearchOption.AllDirectories))
						{
							hashSet.Add(Path.GetFullPath(item3));
						}
					}
					foreach (string item4 in hashSet.OrderBy<string, string>((string x) => x, StringComparer.OrdinalIgnoreCase))
					{
						if (!Same(item4, ownConfig))
						{
							string text = Logical(item4, Paths.ConfigPath);
							try
							{
								SortedDictionary<string, string> sortedDictionary2 = new SortedDictionary<string, string>(StringComparer.Ordinal);
								if (!File.Exists(item4))
								{
									goto IL_01bb;
								}
								if (new FileInfo(item4).Length > 8388608)
								{
									throw new InvalidDataException("Config exceeds 8 MiB.");
								}
								string text2 = Path.GetExtension(item4).ToLowerInvariant();
								if (text2 == ".cfg" || text2 == ".ini")
								{
									foreach (KeyValuePair<string, string> item5 in Ini.Parse(File.ReadAllText(item4)))
									{
										sortedDictionary2[item5.Key] = item5.Value;
									}
									goto IL_01bb;
								}
								Add("config-file", text, FileHash(item4, force), "SHA256 " + FileHash(item4, force).Substring(0, 12));
								goto end_IL_00c4;
								IL_01bb:
								if (configs.TryGetValue(item4, out SortedDictionary<string, string> value2))
								{
									foreach (KeyValuePair<string, string> item6 in value2)
									{
										sortedDictionary2[item6.Key] = item6.Value;
									}
								}
								foreach (KeyValuePair<string, string> item7 in sortedDictionary2)
								{
									Add("config", text + "/" + item7.Key.Replace('\0', '/'), item7.Value, Ini.Display(item7.Key, item7.Value));
								}
								end_IL_00c4:;
							}
							catch (Exception ex)
							{
								errors.Add("Config " + text + ": " + ex.Message);
							}
						}
					}
					string[] array = new string[3]
					{
						Paths.PluginPath,
						Path.Combine(Paths.BepInExRootPath, "patchers"),
						Path.Combine(Paths.BepInExRootPath, "core")
					};
					foreach (string path in array)
					{
						if (Directory.Exists(path))
						{
							foreach (string item8 in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).OrderBy<string, string>((string x) => x, StringComparer.OrdinalIgnoreCase))
							{
								if (!Same(item8, ownConfig))
								{
									string text3 = Path.GetExtension(item8).ToLowerInvariant();
									if (!(text3 == ".log") && !(text3 == ".pdb") && !new string[6] { "readme.md", "changelog.md", "license", "license.txt", "manifest.json", "icon.png" }.Contains(Path.GetFileName(item8).ToLowerInvariant()))
									{
										string text4 = Logical(item8, Paths.BepInExRootPath);
										try
										{
											string text5 = FileHash(item8, force);
											entries.Add(new ManifestEntry
											{
												Category = "file",
												Key = text4,
												Digest = text5,
												Display = "SHA256 " + text5.Substring(0, 12)
											});
										}
										catch (Exception ex2)
										{
											errors.Add("File " + text4 + ": " + ex2.Message);
										}
									}
								}
							}
						}
					}
					string p = Path.Combine(Paths.ManagedPath, "Assembly-CSharp.dll");
					entries.Add(new ManifestEntry
					{
						Category = "runtime",
						Key = "Assembly-CSharp",
						Digest = FileHash(p, force),
						Display = "Game binary"
					});
				}
				catch (Exception ex3)
				{
					errors.Add("Scan failed: " + ex3.Message);
				}
				Manifest manifest = new Manifest();
				manifest.Entries = entries.ToArray();
				manifest.Errors = errors.Take(200).ToArray();
				manifest.Seal();
				return manifest;
			});
			void Add(string c, string k, string text, string display)
			{
				entries.Add(new ManifestEntry
				{
					Category = c,
					Key = k,
					Digest = Hash.Text(text),
					Display = display
				});
			}
		}

		private string FileHash(string p, bool force)
		{
			FileInfo fileInfo = new FileInfo(p);
			if (!force && cache.TryGetValue(p, out (long, long, string) value) && value.Item1 == fileInfo.Length && value.Item2 == fileInfo.LastWriteTimeUtc.Ticks)
			{
				return value.Item3;
			}
			string text = Hash.File(p);
			cache[p] = (fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks, text);
			return text;
		}

		private static bool Same(string a, string b)
		{
			return ConfigPolicy.IsOwn(a, b);
		}

		private static string Logical(string path, string root)
		{
			return Path.GetRelativePath(root, path).Replace('\\', '/').ToLowerInvariant();
		}
	}
	internal static class Access
	{
		private static readonly Dictionary<string, FieldInfo> fields = new Dictionary<string, FieldInfo>();

		public static FieldInfo Field(Type t, string name)
		{
			string text = t.FullName + "." + name;
			if (!fields.TryGetValue(text, out FieldInfo value))
			{
				Dictionary<string, FieldInfo> dictionary = fields;
				FieldInfo obj = AccessTools.Field(t, name) ?? throw new MissingFieldException(text);
				value = obj;
				dictionary[text] = obj;
			}
			return value;
		}

		public static T Read<T>(object o, string name)
		{
			return (T)Field(o.GetType(), name).GetValue(o);
		}

		public static void Set(object o, string name, object? value)
		{
			Field(o.GetType(), name).SetValue(o, value);
		}
	}
	internal sealed class StatSnapshot
	{
		private readonly List<Action> restore = new List<Action>();

		public StatSnapshot(object instance)
		{
			FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			foreach (FieldInfo f in fields)
			{
				object value = f.GetValue(instance);
				Dictionary<string, int> di = value as Dictionary<string, int>;
				if (di != null)
				{
					KeyValuePair<string, int>[] copy = di.ToArray();
					restore.Add(delegate
					{
						di.Clear();
						KeyValuePair<string, int>[] array = copy;
						for (int j = 0; j < array.Length; j++)
						{
							KeyValuePair<string, int> keyValuePair = array[j];
							di[keyValuePair.Key] = keyValuePair.Value;
						}
					});
					continue;
				}
				Dictionary<string, string> ds = value as Dictionary<string, string>;
				if (ds != null)
				{
					KeyValuePair<string, string>[] copy2 = ds.ToArray();
					restore.Add(delegate
					{
						ds.Clear();
						KeyValuePair<string, string>[] array = copy2;
						for (int j = 0; j < array.Length; j++)
						{
							KeyValuePair<string, string> keyValuePair = array[j];
							ds[keyValuePair.Key] = keyValuePair.Value;
						}
					});
					continue;
				}
				List<string> ls = value as List<string>;
				if (ls != null)
				{
					string[] copy3 = ls.ToArray();
					restore.Add(delegate
					{
						ls.Clear();
						ls.AddRange(copy3);
					});
					continue;
				}
				List<EnemySetup> le = value as List<EnemySetup>;
				if (le != null)
				{
					EnemySetup[] copy4 = le.ToArray();
					restore.Add(delegate
					{
						le.Clear();
						le.AddRange(copy4);
					});
				}
				else if (value is Level)
				{
					restore.Add(delegate
					{
						f.SetValue(instance, value);
					});
				}
				else if (!f.IsInitOnly && (f.FieldType.IsPrimitive || f.FieldType.IsEnum || f.FieldType == typeof(string)))
				{
					restore.Add(delegate
					{
						f.SetValue(instance, value);
					});
				}
			}
		}

		public void Restore()
		{
			foreach (Action item in restore)
			{
				item();
			}
		}
	}
	internal sealed class GameBridge
	{
		public RoundPlan? Plan;

		public readonly CosmeticTracker Cosmetics = new CosmeticTracker();

		public readonly DeliveryTracker Deliveries = new DeliveryTracker();

		public readonly HuntTracker Hunts = new HuntTracker();

		private readonly Dictionary<ValuableObject, float> startingValues = new Dictionary<ValuableObject, float>();

		private StatSnapshot? stats;

		private StatSnapshot? run;

		private StatSnapshot? progress;

		private SimulationMode physics;

		private float timeScale;

		private int normalMaxPlayers;

		private int normalPhotonMax;

		private State random;

		private readonly List<(NavMeshAgent agent, bool stopped)> agents = new List<(NavMeshAgent, bool)>();

		public bool Active { get; private set; }

		public bool Protected => stats != null;

		public bool Frozen { get; private set; }

		public bool AllowStart { get; private set; }

		public static bool InNativeLobby
		{
			get
			{
				if ((Object)(object)RunManager.instance != (Object)null)
				{
					if (!SemiFunc.RunIsLobbyMenu())
					{
						return SemiFunc.RunIsLobby();
					}
					return true;
				}
				return false;
			}
		}

		public static bool CanOpen
		{
			get
			{
				if ((Object)(object)RunManager.instance != (Object)null && (Object)(object)StatsManager.instance != (Object)null && (Object)(object)GameManager.instance != (Object)null)
				{
					if (!SemiFunc.IsMainMenu() && !SemiFunc.RunIsLobbyMenu())
					{
						return SemiFunc.RunIsLobby();
					}
					return true;
				}
				return false;
			}
		}

		public static int Health
		{
			get
			{
				if (!((Object)(object)PlayerAvatar.instance == (Object)null))
				{
					return Access.Read<int>(PlayerAvatar.instance.playerHealth, "health");
				}
				return 0;
			}
		}

		public static int MaxHealth
		{
			get
			{
				if (!((Object)(object)PlayerAvatar.instance == (Object)null))
				{
					return Access.Read<int>(PlayerAvatar.instance.playerHealth, "maxHealth");
				}
				return 0;
			}
		}

		public static long Delivered => Plugin.Instance.Game.Deliveries.Ledger.Delivered;

		public static string[] Stages()
		{
			if (!((Object)(object)RunManager.instance == (Object)null))
			{
				return (from l in RunManager.instance.levels
					where (Object)(object)l != (Object)null
					select ((Object)l).name).Distinct().OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray();
			}
			return Array.Empty<string>();
		}

		public static string[] Items()
		{
			StatsManager instance = StatsManager.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return Array.Empty<string>();
			}
			if (instance.itemDictionary.Count == 0)
			{
				Item[] array = Resources.LoadAll<Item>(instance.folderPath);
				foreach (Item val in array)
				{
					if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(((Object)val).name))
					{
						instance.itemDictionary.TryAdd(((Object)val).name, val);
					}
				}
			}
			return (from x in instance.itemDictionary
				where (Object)(object)x.Value != (Object)null && !x.Value.disabled
				select x.Key).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray();
		}

		public static string Pose(Transform t)
		{
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			return ((Object)t).name.Replace("(Clone)", "") + "@" + Vector(t.position) + "/" + Vector(t.eulerAngles);
		}

		private static string Vector(Vector3 v)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			return string.Join(",", new float[3] { v.x, v.y, v.z }.Select((float x) => Math.Round(x, 3).ToString("F3", CultureInfo.InvariantCulture)));
		}

		public IEnumerator Load(RoundPlan plan, string self, Action<StageReady> ready, Action<string> error)
		{
			try
			{
				Prepare(plan, self);
			}
			catch (Exception ex)
			{
				error(ex.Message);
				yield break;
			}
			double deadline;
			if (plan.HasTeams && !plan.IsSpectator(self))
			{
				bool failed = false;
				yield return Plugin.Instance.Teams.Enter(plan, delegate(string reason)
				{
					failed = true;
					error(reason);
				});
				if (failed)
				{
					yield break;
				}
			}
			else
			{
				Plugin.Instance.Teams.Reset();
				PhotonNetwork.Disconnect();
				deadline = Transport.Now + 20.0;
				while (PhotonNetwork.IsConnected)
				{
					if (Transport.Now > deadline)
					{
						error("Photon disconnect timed out.");
						yield break;
					}
					yield return null;
				}
			}
			if ((Object)(object)GameDirector.instance != (Object)null)
			{
				GameDirector.instance.currentState = (gameState)0;
			}
			yield return SceneManager.LoadSceneAsync("Main");
			deadline = Transport.Now + 180.0;
			while ((Object)(object)LevelGenerator.Instance == (Object)null || !LevelGenerator.Instance.Generated)
			{
				if (!Active)
				{
					yield break;
				}
				if (Transport.Now > deadline)
				{
					error("Stage generation timed out.");
					yield break;
				}
				yield return null;
			}
			yield return (object)new WaitForSecondsRealtime(1f);
			if (plan.HasTeams && !plan.IsSpectator(self) && !PhotonNetwork.IsMasterClient)
			{
				deadline = Transport.Now + 90.0;
				while (!(((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"sr-stage"] is byte[] array) || array.Length == 0)
				{
					if (Transport.Now > deadline)
					{
						error("Waiting for the team stage timed out.");
						yield break;
					}
					yield return null;
				}
			}
			try
			{
				VerifyLocalAuthority();
				ValuableObject[] array2 = Object.FindObjectsOfType<ValuableObject>();
				if (array2.Any((ValuableObject v) => !Access.Read<bool>(v, "dollarValueSet")))
				{
					throw new InvalidOperationException("Valuable prices are not ready.");
				}
				startingValues.Clear();
				ValuableObject[] array3 = array2;
				foreach (ValuableObject val in array3)
				{
					startingValues.Add(val, Access.Read<float>(val, "dollarValueOriginal"));
				}
				VerifyStartingValuables();
				Deliveries.Capture(array2);
				string[] first = (from x in Object.FindObjectsOfType<ItemAttributes>()
					where (Object)(object)x.item != (Object)null && plan.Items.Contains(((Object)x.item).name)
					select ((Object)x.item).name).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray();
				string[] source = Enumerable.Range(0, (!plan.HasTeams || plan.IsSpectator(self)) ? 1 : plan.Teammates(self).Length).SelectMany((int _) => plan.Items).ToArray();
				if (!first.SequenceEqual(source.OrderBy<string, string>((string x) => x, StringComparer.Ordinal)))
				{
					throw new InvalidOperationException("The generated item quantities do not match the host loadout.");
				}
				if (Health != 100 + 20 * plan.Loadout.Health || MaxHealth != Health)
				{
					throw new InvalidOperationException("Starting health does not match the host loadout.");
				}
				PlayerAvatar instance = PlayerAvatar.instance;
				PlayerController instance2 = PlayerController.instance;
				Loadout loadout = plan.Loadout;
				if (Access.Read<int>(instance2, "JumpExtra") != loadout.ExtraJump || !Mathf.Approximately(instance2.SprintSpeedUpgrades, (float)loadout.Speed) || !Mathf.Approximately(Access.Read<float>(instance, "upgradeCrouchRest"), (float)loadout.CrouchRest) || !Mathf.Approximately(Access.Read<float>(instance, "upgradeTumbleWings"), (float)loadout.TumbleWings) || !Mathf.Approximately(Access.Read<float>(instance, "upgradeTumbleClimb"), (float)loadout.TumbleClimb) || Access.Read<int>(Access.Read<object>(instance, "tumble"), "tumbleLaunch") != loadout.TumbleLaunch)
				{
					throw new InvalidOperationException("Initial movement upgrades do not match the host loadout.");
				}
				Plugin.Log.LogInfo((object)string.Format("Initial upgrades verified: Lv.{0}, {1}, seed={2}, counts={3}, health={4}, stamina={5}", plan.Level, plan.LevelName, plan.Seed, string.Join(",", Upgrades.Values(loadout)), Health, instance2.EnergyStart));
				VerifyStartingStation();
				int num2 = Access.Read<int>(RoundDirector.instance, "extractionPoints");
				if (num2 < 1)
				{
					throw new InvalidOperationException("Extraction points are not ready.");
				}
				List<string> list = new List<string>();
				list.Add(plan.RecordKey);
				list.Add("extraction-count:" + num2);
				list.Add("health:" + Health);
				list.Add("upgrades:" + string.Join(",", Upgrades.Values(loadout)));
				list.Add("charging-station:" + Pose(((Component)ChargingStation.instance).transform) + ":" + 30 + "/" + 3);
				List<string> list2 = list;
				Module[] array4 = Object.FindObjectsOfType<Module>();
				foreach (Module val2 in array4)
				{
					list2.Add("module:" + Pose(((Component)val2).transform));
				}
				array3 = array2;
				foreach (ValuableObject val3 in array3)
				{
					list2.Add("value:" + Pose(((Component)val3).transform) + ":" + Access.Read<float>(val3, "dollarValueOriginal").ToString("R", CultureInfo.InvariantCulture) + "/" + Access.Read<float>(val3, "dollarValueCurrent").ToString("R", CultureInfo.InvariantCulture));
				}
				EnemyParent[] array5 = Object.FindObjectsOfType<EnemyParent>();
				foreach (EnemyParent val4 in array5)
				{
					list2.Add("enemy:" + Pose(((Component)val4).transform));
				}
				ExtractionPoint[] array6 = Object.FindObjectsOfType<ExtractionPoint>();
				foreach (ExtractionPoint val5 in array6)
				{
					list2.Add("extraction:" + Pose(((Component)val5).transform));
				}
				if (!plan.HasTeams || plan.IsSpectator(self))
				{
					ItemAttributes[] array7 = Object.FindObjectsOfType<ItemAttributes>();
					foreach (ItemAttributes val6 in array7)
					{
						list2.Add("item:" + ((Object)val6.item).name + ":" + Pose(((Component)val6).transform));
					}
				}
				if (!plan.HasTeams || plan.IsSpectator(self))
				{
					PhysGrabObject[] array8 = Object.FindObjectsOfType<PhysGrabObject>();
					foreach (PhysGrabObject val7 in array8)
					{
						list2.Add("physics:" + Pose(((Component)val7).transform));
						Rigidbody component = ((Component)val7).GetComponent<Rigidbody>();
						if ((Object)(object)component != (Object)null)
						{
							list2.Add("mass:" + Pose(((Component)val7).transform) + ":" + component.mass.ToString("R", CultureInfo.InvariantCulture));
						}
					}
				}
				ItemBattery[] array9 = Object.FindObjectsOfType<ItemBattery>();
				foreach (ItemBattery val8 in array9)
				{
					if (Math.Abs(val8.batteryLife - 100f) > 0.001f)
					{
						throw new InvalidOperationException("An initial item battery is not full.");
					}
					if (!plan.HasTeams || plan.IsSpectator(self))
					{
						list2.Add("battery:" + Pose(((Component)val8).transform) + ":" + val8.batteryLife.ToString("R", CultureInfo.InvariantCulture));
					}
				}
				list2.AddRange(Cosmetics.Capture());
				if (!plan.HasTeams || plan.IsSpectator(self) || PhotonNetwork.IsMasterClient)
				{
					list2.AddRange(Hunts.Capture());
					Plugin.Log.LogInfo((object)("Cosmetic boxes captured: " + Cosmetics.Initial.Length + "; full-health bonus: " + Cosmetics.ReferenceWeight * 50));
					if (!plan.HasTeams || plan.IsSpectator(self))
					{
						list2.Add("spawn:" + Pose(((Component)PlayerAvatar.instance).transform));
					}
					list2.Add("goal:" + Access.Read<int>(RoundDirector.instance, "haulGoal") + ":" + Access.Read<int>(RoundDirector.instance, "haulGoalMax"));
					long num3 = array2.Sum((ValuableObject v) => (long)Access.Read<float>(v, "dollarValueOriginal"));
					if (num3 <= 0)
					{
						throw new InvalidOperationException("No initial valuable value.");
					}
					StageReady stageReady = new StageReady
					{
						Hash = Hash.Fields(list2.OrderBy<string, string>((string x) => x, StringComparer.Ordinal)),
						Value = num3,
						Health = MaxHealth,
						ExtractionPoints = num2,
						CosmeticBoxes = Cosmetics.Initial,
						EnemyGroups = Hunts.Initial
					};
					if (plan.HasTeams && !plan.IsSpectator(self))
					{
						Room currentRoom = PhotonNetwork.CurrentRoom;
						Hashtable val9 = new Hashtable();
						((Dictionary<object, object>)val9).Add((object)"sr-stage", (object)CheckpointPayload.Pack(Codec.Write(new TeamStage
						{
							Stage = stageReady,
							Hunts = Hunts.Snapshot()
						})));
						currentRoom.SetCustomProperties(val9, (Hashtable)null, (WebFlags)null);
					}
					ready(stageReady);
				}
				else
				{
					TeamStage teamStage = Codec.Read<TeamStage>(CheckpointPayload.Unpack((byte[])((RoomInfo)PhotonNetwork.CurrentRoom).CustomProperties[(object)"sr-stage"]));
					Hunts.Restore(teamStage.Hunts);
					if (teamStage.Stage.Value != array2.Sum((ValuableObject v) => (long)Access.Read<float>(v, "dollarValueOriginal")) || teamStage.Stage.Health != MaxHealth || teamStage.Stage.ExtractionPoints != num2)
					{
						throw new InvalidOperationException("Team stage values differ.");
					}
					ready(teamStage.Stage);
				}
			}
			catch (Exception ex2)
			{
				error(ex2.Message);
			}
		}

		public void Observe(RoundPlan plan)
		{
			CaptureNormalState();
			Active = true;
			Frozen = true;
			AllowStart = false;
			Plan = plan;
			agents.Clear();
			Cosmetics.Clear();
			Deliveries.Clear();
			Hunts.Clear();
			startingValues.Clear();
			Physics.simulationMode = (SimulationMode)2;
			Time.timeScale = 1f;
			Plugin.Instance.Teams.Reset();
			NetworkConnect[] array = Object.FindObjectsOfType<NetworkConnect>();
			foreach (NetworkConnect obj in array)
			{
				((MonoBehaviour)obj).StopAllCoroutines();
				Object.Destroy((Object)(object)((Component)obj).gameObject);
			}
			PhotonNetwork.AutomaticallySyncScene = false;
			PhotonNetwork.Disconnect();
			GameManager.instance.SetGameMode(0);
			HUD instance = HUD.instance;
			if (instance != null)
			{
				instance.Show();
			}
			if ((Object)(object)MenuManager.instance != (Object)null)
			{
				MenuManager.instance.PageCloseAll();
			}
		}

		public void OpenPrivateLobby()
		{
			if (Active || InNativeLobby)
			{
				return;
			}
			if (!CanOpen || (Object)(object)MainMenuOpen.instance == (Object)null)
			{
				throw new InvalidOperationException("Private lobby must open from the main menu.");
			}
			CaptureNormalState();
			try
			{
				GameManager.instance.SetMaxPlayers(150);
				GameManager.instance.localTest = false;
				RunManager.instance.ResetProgress();
				GameManager.instance.SetConnectRandom(false);
				GameManager.instance.SetLobbyType((LobbyTypes)0);
				StatsManager instance = StatsManager.instance;
				Access.Set(instance, "saveFileCurrent", "");
				Access.Set(instance, "saveFileReady", false);
				instance.teamName = "ScoreRace";
				instance.teamNameChanged = true;
				AccessTools.Method(typeof(StatsManager), "PlayersAddAll", (Type[])null, (Type[])null).Invoke(instance, null);
				Access.Set(RunManager.instance, "waitToChangeScene", true);
				RunManager.instance.ChangeLevel(true, false, (ChangeLevelType)3);
				MainMenuOpen.instance.NetworkConnect();
			}
			catch
			{
				Restore();
				throw;
			}
		}

		public void CaptureNormalState()
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: 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)
			if (!Protected)
			{
				stats = new StatSnapshot(StatsManager.instance);
				run = new StatSnapshot(RunManager.instance);
				if ((Object)(object)ProgressionManager.instance != (Object)null)
				{
					progress = new StatSnapshot(ProgressionManager.instance);
				}
				physics = Physics.simulationMode;
				timeScale = Time.timeScale;
				random = Random.state;
				normalMaxPlayers = GameManager.instance.maxPlayers;
				normalPhotonMax = GameManager.instance.maxPlayersPhoton;
			}
		}

		private void Prepare(RoundPlan plan, string self)
		{
			if (!Stages().Contains(plan.LevelName) || plan.Items.Any((string x) => !Items().Contains(x)) || !Items().Contains("Item Power Crystal"))
			{
				throw new InvalidOperationException("Host stage/item catalog is unavailable.");
			}
			CaptureNormalState();
			Active = true;
			Frozen = true;
			AllowStart = false;
			Plan = plan;
			agents.Clear();
			Cosmetics.Clear();
			Deliveries.Clear();
			Hunts.Clear();
			startingValues.Clear();
			Physics.simulationMode = (SimulationMode)2;
			Time.timeScale = 1f;
			GameManager.instance.SetGameMode((plan.HasTeams && !plan.IsSpectator(self)) ? 1 : 0);
			StatsManager instance = StatsManager.instance;
			FieldInfo[] fields = typeof(StatsManager).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			foreach (FieldInfo fieldInfo in fields)
			{
				if (fieldInfo.GetValue(instance) is Dictionary<string, int> dictionary && fieldInfo.Name != "stripTheseDictionaries")
				{
					dictionary.Clear();
				}
			}
			Access.Read<List<string>>(instance, "takenItemNames").Clear();
			Access.Set(instance, "saveFileReady", false);
			instance.runStats["level"] = plan.Level - 1;
			instance.runStats["currency"] = 0;
			instance.runStats["lives"] = 1;
			instance.runStats["chargingStationCharge"] = 3;
			instance.runStats["chargingStationChargeTotal"] = 30;
			instance.runStats["totalHaul"] = 0;
			string[] array = plan.Teammates(self);
			foreach (string key in array)
			{
				instance.playerUpgradeHealth[key] = plan.Loadout.Health;
				instance.playerUpgradeStamina[key] = plan.Loadout.Stamina;
				instance.playerUpgradeSpeed[key] = plan.Loadout.Speed;
				instance.playerUpgradeStrength[key] = plan.Loadout.Strength;
				instance.playerUpgradeRange[key] = plan.Loadout.Range;
				instance.playerHealth[key] = 100 + plan.Loadout.Health * 20;
				instance.playerUpgradeCrouchRest[key] = plan.Loadout.CrouchRest;
				instance.playerUpgradeExtraJump[key] = plan.Loadout.ExtraJump;
				instance.playerUpgradeLaunch[key] = plan.Loadout.TumbleLaunch;
				instance.playerUpgradeTumbleWings[key] = plan.Loadout.TumbleWings;
				instance.playerUpgradeTumbleClimb[key] = plan.Loadout.TumbleClimb;
			}
			foreach (IGrouping<string, string> item in from x in plan.Items
				group x by x)
			{
				instance.itemsPurchased[item.Key] = item.Count() * ((!plan.HasTeams || plan.IsSpectator(self)) ? 1 : plan.Teammates(self).Length);
				instance.itemsPurchasedTotal[item.Key] = instance.itemsPurchased[item.Key];
			}
			instance.itemsPurchased["Item Power Crystal"] = 3;
			instance.itemsPurchasedTotal["Item Power Crystal"] = 3;
			ItemManager.instance.ResetAllItems();
			ItemManager.instance.spawnedItems.Clear();
			ItemManager.instance.localPlayerInventory.Clear();
			RunManager instance2 = RunManager.instance;
			Access.Set(instance2, "runLives", 1);
			Access.Set(instance2, "runStarted", false);
			instance2.levelCurrent = instance2.levels.First((Level x) => ((Object)x).name == plan.LevelName);
			instance2.levelsCompleted = plan.Level - 1;
			array = new string[9] { "restarting", "restartingDone", "waitToChangeScene", "lobbyJoin", "levelFailed", "gameOver", "allPlayersDead", "masterSwitched", "levelIsShop" };
			foreach (string name in array)
			{
				Access.Set(instance2, name, false);
			}
			Access.Set(instance2, "levelPrevious", instance2.levelCurrent);
			Access.Set(instance2, "previousRunLevel", instance2.levelCurrent);
			Access.Set(instance2, "loadLevel", 0);
			Access.Set(instance2, "saveLevel", 0);
			Access.Set(instance2, "cosmeticWorldObjectCooldown", 0f);
			Access.Set(instance2, "cosmeticWorldObjectBadLuckCount", 0);
			instance2.enemiesSpawned.Clear();
			Access.Read<List<EnemySetup>>(instance2, "enemiesSpawnedToDelete").Clear();
			instance2.UpdateMoonLevel();
			Determinism.Reset();
		}

		public void Warmup()
		{
			if (Active)
			{
				AllowStart = true;
				GameDirector.instance.SetStart();
			}
		}

		public void HoldAgents()
		{
			if (!Active || !Frozen)
			{
				return;
			}
			NavMeshAgent[] array = Object.FindObjectsOfType<NavMeshAgent>();
			foreach (NavMeshAgent a in array)
			{
				if (((Behaviour)a).enabled && a.isOnNavMesh && !agents.Any<(NavMeshAgent, bool)>(((NavMeshAgent agent, bool stopped) x) => (Object)(object)x.agent == (Object)(object)a))
				{
					agents.Add((a, a.isStopped));
					a.isStopped = true;
				}
			}
			Physics.simulationMode = (SimulationMode)2;
		}

		private void VerifyStartingValuables()
		{
			foreach (KeyValuePair<ValuableObject, float> startingValue in startingValues)
			{
				ValuableObject key = startingValue.Key;
				if ((Object)(object)key == (Object)null || !Access.Read<bool>(key, "dollarValueSet") || !Rules.Finite(startingValue.Value) || startingValue.Value < 0f || Access.Read<float>(key, "dollarValueOriginal") != startingValue.Value || Access.Read<float>(key, "dollarValueCurrent") != startingValue.Value)
				{
					throw new InvalidOperationException("A starting valuable was damaged, removed or changed before the race.");
				}
			}
		}

		private void VerifyStartingStation()
		{
			ChargingStation instance = ChargingStation.instance;
			if ((Object)(object)instance == (Object)null || Access.Read<int>(instance, "chargeTotal") != 30 || Access.Read<int>(instance, "chargeInt") != 3 || !Mathf.Approximately(Access.Read<float>(instance, "chargeFloat"), 0.3f))
			{
				throw new InvalidOperationException("The charging station must start at 30% with three installed crystals.");
			}
		}

		public static void VerifyLocalAuthority()
		{
			if (Plugin.Instance.Teams.Enabled)
			{
				if ((Object)(object)GameManager.instance == (Object)null || GameManager.instance.gameMode != 1 || !PhotonNetwork.InRoom)
				{
					throw new InvalidOperationException("The team stage connection was lost.");
				}
			}
			else if ((Object)(object)GameManager.instance == (Object)null || GameManager.instance.gameMode != 0 || PhotonNetwork.IsConnected)
			{
				throw new InvalidOperationException("The race stage must run in a disconnected local simulation.");
			}
		}

		public void Release()
		{
			//IL_0171: Unknown result type (might be due to invalid IL or missing references)
			VerifyLocalAuthority();
			VerifyStartingValuables();
			VerifyStartingStation();
			float[] array = (from v in startingValues.Keys
				select ((Component)v).GetComponent<PhysGrabObjectImpactDetector>() into d
				where (Object)(object)d != (Object)null
				select Access.Read<float>(d, "indestructibleSpawnTimer")).ToArray();
			Plugin.Log.LogInfo((object)("Valuable start verified: " + startingValues.Count + " full value; minimum native spawn protection=" + ((array.Length != 0) ? array.Min() : 0f).ToString("F2", CultureInfo.InvariantCulture) + "s."));
			foreach (var agent in agents)
			{
				if ((Object)(object)agent.agent != (Object)null && ((Behaviour)agent.agent).enabled && agent.agent.isOnNavMesh)
				{
					agent.agent.isStopped = agent.stopped;
				}
			}
			agents.Clear();
			Physics.simulationMode = physics;
			Frozen = false;
			Plugin.Log.LogInfo((object)("Native menu key: " + InputManager.instance.GetKeyString((InputKey)6)));
		}

		public void ResumeSimulation()
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			foreach (var agent in agents)
			{
				if ((Object)(object)agent.agent != (Object)null && ((Behaviour)agent.agent).enabled && agent.agent.isOnNavMesh)
				{
					agent.agent.isStopped = agent.stopped;
				}
			}
			agents.Clear();
			Physics.simulationMode = physics;
			Frozen = false;
		}

		public void Freeze()
		{
			if (Active)
			{
				Frozen = true;
				Physics.simulationMode = (SimulationMode)2;
			}
		}

		public void PurchaseItems()
		{
			ItemManager instance = ItemManager.instance;
			instance.purchasedItems.Clear();
			foreach (string item in Enumerable.Range(0, (!Plan.HasTeams || Plan.IsSpectator(Plugin.Instance.Race.Net.Self)) ? 1 : Plan.Teammates(Plugin.Instance.Race.Net.Self).Length).SelectMany((int _) => Plan.Items))
			{
				instance.purchasedItems.Add(StatsManager.instance.itemDictionary[item]);
			}
		}

		public void SpawnItems()
		{
			//IL_017b: Unknown result type (might be due to invalid IL or missing references)
			//IL_014c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0158: Unknown result type (might be due to invalid IL or missing references)
			//IL_015e: Invalid comparison between Unknown and I4
			//IL_0180: 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_0218: Unknown result type (might be due to invalid IL or missing references)
			//IL_021a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0226: Unknown result type (might be due to invalid IL or missing references)
			//IL_0232: Unknown result type (might be due to invalid IL or missing references)
			//IL_0237: Unknown result type (might be due to invalid IL or missing references)
			//IL_016c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0174: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_01db: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
			ItemManager instance = ItemManager.instance;
			if (Plan.HasTeams && !Plan.IsSpectator(Plugin.Instance.Race.Net.Self) && !PhotonNetwork.IsMasterClient)
			{
				return;
			}
			ItemVolume[] array = instance.itemVolumes.OrderBy<ItemVolume, string>((ItemVolume v) => Pose(((Component)v).transform), StringComparer.Ordinal).ToArray();
			Dictionary<ItemVolume, int> counts = new Dictionary<ItemVolume, int>();
			instance.spawnedItems.Clear();
			foreach (Item item in instance.purchasedItems)
			{
				ItemVolume[] array2 = array.Where((ItemVolume v) => v.itemVolume == item.itemVolume).ToArray();
				if (array2.Length == 0)
				{
					throw new InvalidOperationException("No truck volume for " + ((Object)item).name);
				}
				int value2;
				ItemVolume val = array2.OrderBy((ItemVolume v) => counts.TryGetValue(v, out value2) ? value2 : 0).First();
				int value;
				int num = (counts.TryGetValue(val, out value) ? value : 0);
				counts[val] = num + 1;
				Vector3 val2 = ((num == 0) ? Vector3.zero : (((Component)val).transform.right * (((int)item.itemType == 2) ? 2.2f : 0.65f) * (float)num));
				if (Plan.HasTeams && !Plan.IsSpectator(Plugin.Instance.Race.Net.Self))
				{
					PhotonNetwork.InstantiateRoomObject(((PrefabRef<GameObject>)(object)item.prefab).ResourcePath, ((Component)val).transform.position + val2, ((Component)val).transform.rotation * item.spawnRotationOffset, (byte)0, (object[])null);
				}
				else
				{
					Object.Instantiate<GameObject>(((PrefabRef<GameObject>)(object)item.prefab).Prefab, ((Component)val).transform.position + val2, ((Component)val).transform.rotation * item.spawnRotationOffset);
				}
			}
			ItemVolume[] array3 = array;
			for (int num2 = 0; num2 < array3.Length; num2++)
			{
				Object.Destroy((Object)(object)((Component)array3[num2]).gameObject);
			}
			instance.itemVolumes.Clear();
		}

		public IEnumerator ReturnToLobby(Action done, Action<string> error)
		{
			if (PhotonNetwork.IsConnected)
			{
				PhotonNetwork.Disconnect();
				while (PhotonNetwork.IsConnected)
				{
					yield return null;
				}
			}
			Plugin.Instance.Teams.Reset();
			Active = false;
			Frozen = false;
			AllowStart = false;
			Plan = null;
			agents.Clear();
			Cosmetics.Clear();
			Deliveries.Clear();
			Hunts.Clear();
			startingValues.Clear();
			Physics.simulationMode = physics;
			Time.timeScale = 1f;
			GameManager.instance.SetMaxPlayers(150);
			GameManager.instance.SetGameMode(0);
			GameManager.instance.localTest = false;
			GameManager.instance.SetConnectRandom(false);
			GameManager.instance.SetLobbyType((LobbyTypes)0);
			RunManager.instance.ResetProgress();
			Access.Set(StatsManager.instance, "saveFileCurrent", "");
			Access.Set(StatsManager.instance, "saveFileReady", false);
			StatsManager.instance.teamName = "ScoreRace";
			StatsManager.instance.teamNameChanged = true;
			AccessTools.Method(typeof(StatsManager), "PlayersAddAll", (Type[])null, (Type[])null).Invoke(StatsManager.instance, null);
			RunManager.instance.levelCurrent = RunManager.instance.levelLobbyMenu;
			string[] array = new string[6] { "restarting", "restartingDone", "waitToChangeScene", "lobbyJoin", "allPlayersDead", "gameOver" };
			foreach (string name in array)
			{
				Access.Set(RunManager.instance, name, false);
			}
			if ((Object)(object)GameDirector.instance != (Object)null)
			{
				GameDirector.instance.currentState = (gameState)0;
			}
			yield return SceneManager.LoadSceneAsync("LobbyJoin");
			double deadline = Transport.Now + 90.0;
			while (!PhotonNetwork.InRoom)
			{
				if (Transport.Now > deadline)
				{
					error("Reconnecting to the native lobby timed out.");
					yield break;
				}
				yield return null;
			}
			if (PhotonNetwork.IsMasterClient)
			{
				GameManager.instance.SetGameMode(1);
				PhotonNetwork.LoadLevel("Main");
			}
			while (!InNativeLobby || !PhotonNetwork.InRoom || (Object)(object)MenuPageLobby.instance == (Object)null)
			{
				if (Transport.Now > deadline)
				{
					error("Returning to the native lobby timed out.");
					yield break;
				}
				yield return null;
			}
			if (PhotonNetwork.IsMasterClient)
			{
				SteamManager.instance.UnlockLobby(false);
			}
			done();
		}

		public IEnumerator Exit(Action done)
		{
			if (!Active)
			{
				Restore();
				done();
				yield break;
			}
			Frozen = true;
			AllowStart = false;
			PhotonNetwork.Disconnect();
			while (PhotonNetwork.IsConnected)
			{
				yield return null;
			}
			Plugin.Instance.Teams.Reset();
			if ((Object)(object)SteamManager.instance != (Object)null)
			{
				SteamManager.instance.LeaveLobby();
			}
			GameManager.instance.SetGameMode(0);
			RunManager.instance.levelCurrent = RunManager.instance.levelMainMenu;
			string[] array = new string[6] { "restarting", "restartingDone", "waitToChangeScene", "lobbyJoin", "allPlayersDead", "gameOver" };
			foreach (string name in array)
			{
				Access.Set(RunManager.instance, name, false);
			}
			if ((Object)(object)GameDirector.instance != (Object)null)
			{
				GameDirector.instance.currentState = (gameState)0;
			}
			yield return SceneManager.LoadSceneAsync("Main");
			yield return null;
			Restore();
			RunManager.instance.levelCurrent = RunManager.instance.levelMainMenu;
			done();
		}

		public void Restore()
		{
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			if (Protected || Active)
			{
				stats?.Restore();
				run?.Restore();
				progress?.Restore();
				stats = null;
				run = null;
				progress = null;
				if ((Object)(object)GameManager.instance != (Object)null)
				{
					GameManager.instance.maxPlayers = normalMaxPlayers;
					GameManager.instance.maxPlayersPhoton = normalPhotonMax;
				}
				Physics.simulationMode = physics;
				Time.timeScale = timeScale;
				Random.state = random;
				Active = false;
				Frozen = false;
				AllowStart = false;
				Plan = null;
				agents.Clear();
				Cosmetics.Clear();
				Deliveries.Clear();
				Hunts.Clear();
				startingValues.Clear();
			}
		}
	}
	internal sealed class TeamStage
	{
		public StageReady Stage = new StageReady();

		public HuntSnapshot Hunts = new HuntSnapshot();
	}
	internal sealed class HuntTracker
	{
		internal sealed class Spawn
		{
			public EnemySetup Setup;

			public readonly List<EnemyParent> Parents = new List<EnemyParent>();
		}

		private readonly Stack<Spawn> spawning = new Stack<Spawn>();

		private readonly List<EnemyGroup> groups = new List<EnemyGroup>();

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

		private readonly HashSet<int> observedDeaths = new HashSet<int>();

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

		public HuntLedger Ledger = new HuntLedger(Array.Empty<EnemyGroup>());

		public EnemyGroup[] Initial => groups.ToArray();

		public void Clear()
		{
			spawning.Clear();
			groups.Clear();
			members.Clear();
			observedDeaths.Clear();
			lives.Clear();
			Ledger = new HuntLedger(Array.Empty<EnemyGroup>());
		}

		public HuntSnapshot Snapshot()
		{
			return new HuntSnapshot
			{
				Groups = Initial,
				Members = new Dictionary<int, string>(members),
				Deaths = observedDeaths.ToArray(),
				Lives = new Dictionary<string, int>(lives),
				Killed = Ledger.Killed,
				Observed = Ledger.ObservedLives
			};
		}

		public void Restore(HuntSnapshot s)
		{
			if (!Hunts.ValidGroups(s.Groups) || s.Members.Count > 1024 || s.Deaths.Length > 1024 || s.Lives.Count > 1024)
			{
				throw new InvalidOperationException("Invalid team hunt checkpoint.");
			}
			Clear();
			groups.AddRange(s.Groups);
			foreach (KeyValuePair<int, string> member in s.Members)
			{
				members[member.Key] = member.Value;
			}
			int[] deaths = s.Deaths;
			foreach (int item in deaths)
			{
				observedDeaths.Add(item);
			}
			foreach (KeyValuePair<string, int> life in s.Lives)
			{
				lives[life.Key] = life.Value;
			}
			Ledger = new HuntLedger(Initial);
			Ledger.Restore(s.Killed, s.Observed);
		}

		public Spawn Begin(EnemySetup setup)
		{
			Spawn spawn = new Spawn
			{
				Setup = setup
			};
			spawning.Push(spawn);
			return spawn;
		}

		public void Created(EnemyParent parent)
		{
			if (spawning.Count > 0)
			{
				spawning.Peek().Parents.Add(parent);
			}
		}

		public void End(Spawn s, bool succeeded)
		{
			if (spawning.Count == 0 || spawning.Pop() != s)
			{
				throw new InvalidOperationException("Enemy group spawn scope changed.");
			}
			if (!succeeded || s.Parents.Count == 0)
			{
				return;
			}
			EnemyDirector instance = EnemyDirector.instance;
			int tier = (instance.enemiesDifficulty3.Contains(s.Setup) ? 3 : (instance.enemiesDifficulty2.Contains(s.Setup) ? 2 : (instance.enemiesDifficulty1.Contains(s.Setup) ? 1 : s.Parents.Max((EnemyParent e) => e.difficulty + 1))));
			string text = Hash.Text("enemy-group:" + groups.Count + ":" + ((Object)s.Setup).name);
			List<string> list = new List<string>();
			for (int num = 0; num < s.Parents.Count; num++)
			{
				EnemyParent val = s.Parents[num];
				string text2 = Hash.Text(text + ":" + num + ":" + ((Object)val).name.Replace("(Clone)", ""));
				list.Add(text2);
				EnemyHealth[] componentsInChildren = ((Component)val).GetComponentsInChildren<EnemyHealth>(true);
				foreach (EnemyHealth obj in componentsInChildren)
				{
					members.Add(TeamRuntime.ObjectId((Component)(object)obj), text2);
				}
			}
			groups.Add(new EnemyGroup
			{
				Id = text,
				Tier = tier,
				Members = list.ToArray()
			});
		}

		public IEnumerable<string> Capture()
		{
			if (spawning.Count != 0)
			{
				throw new InvalidOperationException("An enemy group has not finished spawning.");
			}
			Ledger = new HuntLedger(Initial);
			return groups.Select((EnemyGroup g) => "enemy-group:" + g.Id + ":" + g.Tier + ":" + string.Join(",", g.Members));
		}

		public void Spawned(EnemyHealth health)
		{
			int num = TeamRuntime.ObjectId((Component)(object)health);
			if (!Access.Read<bool>(health, "dead") && observedDeaths.Remove(num) && members.TryGetValue(num, out string value))
			{
				lives.TryGetValue(value, out var value2);
				lives[value] = value2 + 1;
			}
		}

		public void Defeated(EnemyHealth health)
		{
			int num = TeamRuntime.ObjectId((Component)(object)health);
			if (Access.Read<bool>(health, "dead") && members.TryGetValue(num, out string value) && observedDeaths.Add(num))
			{
				lives.TryGetValue(value, out var value2);
				Ledger.Defeat(value, value2);
			}
		}
	}
	internal sealed class HuntSnapshot
	{
		public EnemyGroup[] Groups = Array.Empty<EnemyGroup>();

		public Dictionary<int, string> Members = new Dictionary<int, string>();

		public int[] Deaths = Array.Empty<int>();

		public Dictionary<string, int> Lives = new Dictionary<string, int>();

		public string[] Killed = Array.Empty<string>();

		public string[] Observed = Array.Empty<string>();
	}
	internal sealed class ObserverRoster
	{
		public string Round = "";

		public string[] Players = Array.Empty<string>();
	}
	internal sealed class ObserverRequest
	{
		public string Round = "";

		public string Environment = "";
	}
	internal sealed class ObserverSnapshot
	{
		public RoundPlan Plan = new RoundPlan();

		public StageReady Initial = new StageReady();

		public double Elapsed;

		public string[] Observers = Array.Empty<string>();

		public string[] Departed = Array.Empty<string>();

		public bool Finished;
	}
	internal sealed class Race
	{
		private sealed class SavedRecord
		{
			public Result Result = new Result();
		}

		private sealed class ManifestMessage
		{
			public Manifest Manifest = new Manifest();

			public string Name = "";
		}

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

		private readonly Dictionary<string, Queue<Result>> observerHistory = new Dictionary<string, Queue<Result>>();

		private int historyTurn;

		private string observerOffer = "";

		private string lastObserverRoster = "";

		private double nextObserverRequest;

		private double nextObserverRoster;

		private readonly Plugin plugin;

		private readonly EnvironmentScan scanner;

		public readonly Transport Net = new Transport();

		public Phase State;

		public string Session = "";

		public string Status = "Use Singleplayer or Start in the standard lobby.";

		public string HostSettings = "";

		public PeerRow[] Rows = Array.Empty<PeerRow>();

		public RoundPlan? Plan;

		public TeamDefinition[] LobbyTeams = Array.Empty<TeamDefinition>();

		public string[] LobbySpectators = Array.Empty<string>();

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

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

		public StageReady? Initial;

		public readonly Dictionary<string, Result> Results = new Dictionary<string, Result>();

		public bool DeathObserved;

		public long Damage;

		public readonly ResultNavigation Navigation = new ResultNavigation();

		public readonly ResultPages Pages = new ResultPages();

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

		private bool returningToLobby;

		private bool startCommitted;

		private ResultWait? resultWait;

		private int resultDisplayFrame = -1;

		private bool ownResultShown;

		private bool shownAcknowledged;

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

		private double nextResultSend;

		private double nextNetworkWarning;

		private ResultBarrier? transition;

		private ResultAction pendingAction;

		public string Best = "";

		private readonly LobbyStartRequest startRequest = new LobbyStartRequest();

		private string lastLobbyDiagnostic = "";

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

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

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

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

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

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

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

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

		private Manifest? local;

		private Task<Manifest>? scan;

		private Action? afterScan;

		private double nextBeat;

		private double nextScan;

		private double phaseTime;

		private double startLocal;

		private double lastElapsed;

		private double offset;

		private double bestRtt = double.MaxValue;

		private int clockSamples;

		private int lastHealth;

		private bool exiting;

		private bool waitingReady;

		private bool wasNativeLobby;

		private double nextAutoReady;

		private string sessionLobby = "";

		private string roster = "";

		private string lastLobbyPayload = "";

		private double nextLobbyPublish;

		private double lastLobbyPublish;

		public string[] Participants
		{
			get
			{
				if (Plan != null)
				{
					return LateJoin.Participants(Plan, lateSpectators, Net.Members);
				}
				return Net.Members;
			}
		}

		public bool AcceptingSpectators
		{
			get
			{
				if (Multiplayer && startCommitted && Plan != null && Initial != null && State >= Phase.Countdown)
				{
					return State <= Phase.Finished;
				}
				return false;
			}
		}

		public bool Observing => IsSpectator(Net.Self);

		public RaceOptions LobbyOptions
		{
			get
			{
				try
				{
					return Codec.Read<RaceOptions>(HostSettings);
				}
				catch
				{
					return new RaceOptions();
				}
			}
		}

		public bool LocalDead
		{
			get
			{
				if (!Observing)
				{
					RoundPlan? plan = Plan;
					if (plan == null || !plan.HasTeams)
					{
						return DeathObserved;
					}
					return plugin.Teams.LocalDead;
				}
				return true;
			}
		}

		public bool LocalEliminated
		{
			get
			{
				if (!Observing)
				{
					RoundPlan? plan = Plan;
					if (plan == null || !plan.HasTeams)
					{
						return DeathObserved;
					}
					if (!plugin.Teams.Lives.Eliminated)
					{
						return DeathObserved;
					}
					return true;
				}
				return true;
			}
		}

		public long DisplayDamage
		{
			get
			{
				RoundPlan? plan = Plan;
				if (plan == null || !plan.HasTeams)
				{
					return Damage;
				}
				return plugin.Teams.Damage;
			}
		}

		public string RankId
		{
			get
			{
				if (!Observing)
				{
					RoundPlan? plan = Plan;
					if (plan != null && plan.HasTeams)
					{
						return (from r in Results.Values.Where((Result r) => r.TeamNumber == Plan.Team(Net.Self).Number).OrderBy((Result r) => r, Comparer<Result>.Create(Result.Compare))
							select r.PlayerId).FirstOrDefault() ?? Net.Self;
					}
				}
				return Net.Self;
			}
		}

		public double? MatchDeadline => MatchScoring.Deadline(Plan, Results.Values);

		public double Elapsed
		{
			get
			{
				if (State != Phase.Running)
				{
					return lastElapsed;
				}
				return Math.Max(0.0, Transport.Now - startLocal);
			}
		}

		public bool Host => Net.Host == Net.Self;

		public bool Busy => scan != null;

		public bool HasSession => Session.Length > 0;

		public bool FinalResults
		{
			get
			{
				if (Plan != null)
				{
					return Plan.Competitors.All((string id) => Results.ContainsKey(id) || departed.Contains(id));
				}
				return false;
			}
		}

		public bool Multiplayer { get; private set; }

		private string[] RemainingMembers => Net.Members.Where((string id) => (!startCommitted || IsParticipant(id)) && (!departed.Contains(id) || lateSpectators.Contains(id))).ToArray();

		private bool HostResultReady
		{
			get
			{
				if (FinalResults)
				{
					if (Multiplayer && Host)
					{
						return resultWait?.Ready(Transport.Now) ?? false;
					}
					return true;
				}
				return false;
			}
		}

		public string ResultInstruction
		{
			get
			{
				if (!Multiplayer || Host || (Net.Members.Contains(Net.Host) && (!seen.TryGetValue(Net.Host, out var value) || !(Transport.Now - value > 45.0))))
				{
					if (FinalResults)
					{
						if (Host && Multiplayer)
						{
							ResultWait? obj = resultWait;
							if (obj == null || !obj.AllPresented)
							{
								return "Waiting for all result screens to appear. You can change your selection.";
							}
						}
						if (!Host || !Multiplayer || HostResultReady)
						{
							if (Host || !Multiplayer)
							{
								return "Choose what to do next.";
							}
							return "Waiting for the host. MAIN MENU leaves immediately.";
						}
						return ((Navigation.Selection == ResultAction.None) ? "Choose what to do next." : ("Selected: " + ActionName(Navigation.Selection))) + "  |  " + Math.Ceiling(resultWait.Remaining(Transport.Now)) + "s\nYou can change your selection.";
					}
					if (Navigation.Selection != ResultAction.None)
					{
						return "Selected: " + ActionName(Navigation.Selection) + " - waiting for remaining players.\nYou can change your selection.";
					}
					return "Waiting for the remaining players.";
				}
				return "Host unavailable. Your result is saved locally. MAIN MENU leaves immediately.";
			}
		}

		public bool InRound
		{
			get
			{
				if (State >= Phase.Preparing && State <= Phase.Finished)
				{
					return !FinalResults;
				}
				return false;
			}
		}

		public bool ReadySelf => Rows.Any((PeerRow x) => x.Id == Net.Self && x.Ready);

		public bool SetupInvalid { get; private set; }

		public bool StartRequested => startRequest.Pending;

		private string SettingsHash => Hash.Text(HostSettings);

		public bool CanStart
		{
			get
			{
				bool value;
				Manifest value2;
				if (Host && State == Phase.Lobby && HostSettings.Length > 0 && Net.Members.Length != 0 && Net.Members.Length <= 150 && Net.Members.Any((string id) => !LobbySpectators.Contains(id)) && (Net.LobbyId.Length == 0 || Teams.Valid(LobbyTeams, Net.Members.Where((string id) => !LobbySpectators.Contains(id)).ToArray())))
				{
					return Net.Members.All((string id) => votes.TryGetValue(id, out value) && value && manifests.TryGetValue(id, out value2) && local != null && value2.Digest == local.Digest && value2.Verify());
				}
				return false;
			}
		}

		public double Countdown => Math.Max(0.0, startLocal - Transport.Now);

		public bool IsSpectator(string id)
		{
			RoundPlan? plan = Plan;
			if (plan == null || !plan.IsSpectator(id))
			{
				return lateSpectators.Contains(id);
			}
			return true;
		}

		public bool IsParticipant(string id)
		{
			RoundPlan? plan = Plan;
			if (plan == null || !plan.Players.Contains(id))
			{
				return lateSpectators.Contains(id);
			}
			return true;
		}

		public void OpenSpectatorLobby()
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			if (Host && AcceptingSpectators && !((Object)(object)SteamManager.instance == (Object)null))
			{
				Lobby val = Access.Read<Lobby>(SteamManager.instance, "currentLobby");
				SteamId id = ((Lobby)(ref val)).Id;
				if (((SteamId)(ref id)).IsValid && SteamClient.IsValid && ((Lobby)(ref val)).IsOwnedBy(SteamClient.SteamId))
				{
					((Lobby)(ref val)).SetJoinable(true);
				}
			}
		}

		private void ResetObservers()
		{
			observerHistory.Clear();
			historyTurn = 0;
			lateSpectators.Clear();
			observerOffer = (lastObserverRoster = "");
			nextObserverRequest = (nextObserverRoster = 0.0);
		}

		private void ApplyObservers(string[] observers)
		{
			foreach (string item2 in observers.Where((string id) => !lateSpectators.Contains(id)))
			{
				resultAcks.Remove(item2);
			}
			lateSpectators.Clear();
			string[] array = observers;
			foreach (string item in array)
			{
				lateSpectators.Add(item);
			}
			LobbySpectators = LobbySpectators.Concat(observers).Distinct().ToArray();
			array = observers;
			foreach (string text in array)
			{
				roleKnown.Add(text);
				teamChoices.Remove(text);
			}
			if (Plan != null)
			{
				Net.PinnedPeers = Plan.Players.Concat(observers).Distinct().ToArray();
			}
		}

		private void PublishObservers(bool force = false)
		{
			if (Host && AcceptingSpectators)
			{
				string[] array = lateSpectators.Where(((IEnumerable<string>)Net.Members).Contains<string>).OrderBy<string, string>((string id) => id, StringComparer.Ordinal).ToArray();
				ApplyObservers(array);
				string text = Plan.Id + ":" + string.Join("|", array);
				if (force || !(text == lastObserverRoster) || !(Transport.Now < nextObserverRoster))
				{
					lastObserverRoster = text;
					nextObserverRoster = Transport.Now + 5.0;
					Broadcast("observers", new ObserverRoster
					{
						Round = Plan.Id,
						Players = array
					});
				}
			}
		}

		private void ObserverTick(double now)
		{
			if (AcceptingSpectators && Host)
			{
				OpenSpectatorLobby();
				PublishObservers();
				foreach (string item in Net.Members.Where((string id) => id != Net.Self && LateJoin.Eligible(id, Plan, Net.Members, departed) && !lateSpectators.Contains(id)))
				{
					Send(item, "hello", HostSettings);
					Send(item, "observer-offer", Plan.Id);
				}
			}
			if (State == Phase.Lobby && observerOffer.Length > 0 && now >= nextObserverRequest && local != null && local.Verify() && GameBridge.InNativeLobby && PhotonNetwork.InRoom && (Object)(object)HUDCanvas.instance != (Object)null && (!((Object)(object)LoadingUI.instance != (Object)null) || !((Component)LoadingUI.instance).gameObject.activeInHierarchy))
			{
				nextObserverRequest = now + 1.0;
				ToHost("observer-request", new ObserverRequest
				{
					Round = observerOffer,
					Environment = local.Digest
				});
			}
		}

		private void ObserverHistoryTick()
		{
			if (!Host || !AcceptingSpectators)
			{
				observerHistory.Clear();
				return;
			}
			string[] array = observerHistory.Keys.Where((string id) => !Net.Members.Contains(id) || !lateSpectators.Contains(id)).ToArray();
			foreach (string key in array)
			{
				observerHistory.Remove(key);
			}
			string[] array2 = observerHistory.Keys.ToArray();
			for (int num2 = 0; num2 < 2; num2++)
			{
				if (array2.Length == 0)
				{
					break;
				}
				if (Net.PendingPackets >= 256)
				{
					break;
				}
				string text = array2[historyTurn++ % array2.Length];
				Queue<Result> queue = observerHistory[text];
				if (queue.Count == 0)
				{
					continue;
				}
				try
				{
					Wire wire = new Wire
					{
						Session = Session,
						Kind = "observer-result",
						Data = Codec.Write(queue.Peek())
					};
					if (Net.TrySend(text, wire))
					{
						queue.Dequeue();
					}
				}
				catch (Exception ex)
				{
					NetworkIssue(ex);
					break;
				}
			}
		}

		private void AdmitObserver(string sender, ObserverRequest request)
		{
			if (Host && AcceptingSpectators && !(request.Round != Plan.Id) && LateJoin.Eligible(sender, Plan, Net.Members, departed) && manifests.TryGetValue(sender, out Manifest value) && value.Verify() && !(value.Digest != Plan.Environment) && !(request.Environment != Plan.Environment))
			{
				if (!lateSpectators.Contains(sender))
				{
					ApplyObservers(lateSpectators.Concat(new string[1] { sender }).ToArray());
					PublishObservers(force: true);
				}
				Send(sender, "observer-state", new ObserverSnapshot
				{
					Plan = Plan,
					Initial = Initial,
					Elapsed = Math.Max(0.0, Transport.Now - startLocal),
					Observers = lateSpectators.Where(((IEnumerable<string>)Net.Members).Contains<string>).ToArray(),
					Departed = departed.Where(((IEnumerable<string>)Plan.Competitors).Contains<string>).ToArray(),
					Finished = FinalResults
				});
				observerHistory[sender] = new Queue<Result>(Results.Values);
			}
		}

		private void EnterObservation(ObserverSnapshot snapshot)
		{
			if (State != Phase.Lobby || snapshot.Plan.Id != observerOffer || local == null || !local.Verify() || local.Digest != snapshot.Plan.Environment || !GameBridge.InNativeLobby || !PhotonNetwork.InRoom || !LateJoin.ValidSnapshot(snapshot.Plan, snapshot.Initial, snapshot.Elapsed) || !LateJoin.ValidAudience(snapshot.Observers, Net.Members) || !snapshot.Observers.Contains(Net.Self) || snapshot.Departed == null || snapshot.Departed.Any((string id) => !snapshot.Plan.Competitors.Contains(id)))
			{
				return;
			}
			ResetRoundExchange();
			Plan = snapshot.Plan;
			Initial = snapshot.Initial;
			HostSettings = Plan.SettingsJson;
			Multiplayer = true;
			ApplyObservers(snapshot.Observers);
			Navigation.Clear();
			Results.Clear();
			departed.Clear();
			transition = null;
			pendingAction = ResultAction.None;
			string[] array = snapshot.Departed;
			foreach (string item in array)
			{
				departed.Add(item);
			}
			foreach (string item2 in Plan.Competitors.Where((string id) => !Net.Members.Contains(id)))
			{
				departed.Add(item2);
			}
			resultWait = new ResultWait(Plan.Id, Plan.Competitors);
			foreach (string item3 in departed)
			{
				resultWait.Withdraw(item3);
			}
			startLocal = Transport.Now - snapshot.Elapsed;
			lastElapsed = snapshot.Elapsed;
			DeathObserved = false;
			Damage = 0L;
			Best = "";
			plugin.Game.Observe(Plan);
			startCommitted = true;
			SetPhase((snapshot.Finished || FinalResults) ? Phase.Finished : Phase.Running, "Joined as a spectator.");
			foreach (string item4 in Participants.Where((string id) => id != Net.Self))
			{
				Send(item4, "observer-ready", Plan.Id);
			}
		}

		public void ChooseSpectator(bool value)
		{
			if (State == Phase.Lobby && Net.LobbyId.Length > 0)
			{
				ToHost("choose-observer", value);
			}
		}

		public void AssignRole(string player, int slot)
		{
			if (State == Phase.Lobby && (Host || player == Net.Self))
			{
				ToHost("assign-role", new TeamAssignment
				{
					Player = player,
					Slot = slot
				});
			}
		}

		public void ChooseTeam(int number)
		{
			if (State == Phase.Lobby && number >= 1 && number <= 24)
			{
				ToHost("choose-team", number);
			}
		}

		private void SetTeams(RoundPlan plan, RaceOptions options)
		{
			if (plan.Multiplayer)
			{
				plan.Spectators = LobbySpectators.Where(((IEnumerable<string>)plan.Players).Contains<string>).ToArray();
				plan.Teams = Teams.Assign(plan.Competitors, options.TeamCount, options.RandomTeams ? Rules.RandomInt(1, int.MaxValue) : Hash.Seed(Session), teamChoices);
				plan.SharedRevives = options.SharedRevives;
				plan.UnderdogPerMember = options.UnderdogPerMember;
			}
		}

		public void SendPeer(string id, string kind, object value)
		{
			Send(id, kind, value);
		}

		public void ResultPresented()
		{
			if (!Observing && State == Phase.Finished && Plan != null && !ownResultShown)
			{
				if (resultDisplayFrame < 0)
				{
					resultDisplayFrame = Time.frameCount;
				}
				else if (Time.frameCount > resultDisplayFrame)
				{
					ownResultShown = true;
					resultWait.Presented(Net.Self, Plan.Id, Transport.Now);
					SendResultShown();
				}
			}
		}

		private void SendResultShown()
		{
			if (ownResultShown && !Host && !shownAcknowledged && Plan != null)
			{
				Send(Net.Host, "result-shown", Plan.Id);
			}
		}

		private void ResetRoundExchange()
		{
			ResetObservers();
			startRequest.Cancel();
			startCommitted = false;
			resultAcks.Clear();
			nextResultSend = 0.0;
			resultWait = null;
			Pages.Clear();
			resultDisplayFrame = -1;
			ownResultShown = false;
			shownAcknowledged = false;
		}

		private void NetworkIssue(Exception ex)
		{
			if (!startCommitted)
			{
				Fail("Race connection: " + ex.Message, broadcast: false);
			}
			else if (Transport.Now >= nextNetworkWarning)
			{
				nextNetworkWarning = Transport.Now + 10.0;
				plugin.LogMessage("Race communication unavailable; local stage continues: " + ex.GetType().Name);
			}
		}

		private void NetworkTick()
		{
			try
			{
				Net.Tick(Receive);
			}
			catch (Exception ex)
			{
				NetworkIssue(ex);
			}
		}

		private void SendOwnResult(bool leaving = false)
		{
			if (Observing || Plan == null || !Results.TryGetValue(Net.Self, out Result value))
			{
				return;
			}
			foreach (string item in Participants.Where((string id) => id != Net.Self && (leaving || !resultAcks.Contains(id))))
			{
				Send(item, leaving ? "leave-result" : "result", value);
			}
		}

		private static string ActionName(ResultAction action)
		{
			return action switch
			{
				ResultAction.Lobby => "BACK TO LOBBY", 
				ResultAction.Rematch => "REMATCH", 
				_ => "MAIN MENU", 
			};
		}

		public Race(Plugin plugin)
		{
			this.plugin = plugin;
			scanner = new EnvironmentScan(((BaseUnityPlugin)plugin).Config.ConfigFilePath);
		}

		private void SetPhase(Phase state, string status)
		{
			Plugin.Log.LogInfo((object)("ScoreRace phase: " + State.ToString() + " -> " + state.ToString() + ". " + status));
			State = state;
			Status = status;
			phaseTime = Transport.Now;
		}

		private void Send(string to, string kind, object value)
		{
			try
			{
				Net.Send(to, new Wire
				{
					Session = Session,
					Kind = kind,
					Data = Codec.Write(value)
				});
			}
			catch (Exception ex)
			{
				NetworkIssue(ex);
			}
		}

		private void Broadcast(string kind, object value)
		{
			foreach (string item in ((startCommitted && Plan != null) ? Participants : Net.Members).Where((string x) => x != Net.Self))
			{
				Send(item, kind, value);
			}
		}

		private void ToHost(string kind, object value)
		{
			if (Host)
			{
				Receive(Net.Self, new Wire
				{
					Session = Session,
					Kind = kind,
					Data = Codec.Write(value)
				});
			}
			else
			{
				Send(Net.Host, kind, value);
			}
		}

		public void OpenSession()
		{
			if (!GameBridge.CanOpen && !plugin.Game.Active)
			{
				Status = "Start from the main menu or lobby.";
				return;
			}
			Net.Refresh(pinned: false);
			if (!Host)
			{
				Status = "The host must press Start in the standard lobby.";
				return;
			}
			ResetLobby(Guid.NewGuid().ToString("N"));
			Broadcast("hello", HostSettings);
			Scan(force: false, null);
		}

		public void RequestNativeStart(bool solo)
		{
			if (!plugin.Ready || plugin.Game.Active || State == Phase.Preparing || State == Phase.Exiting)
			{
				return;
			}
			Net.Refresh(pinned: false);
			if (!Host)
			{
				Status = "Waiting for the host to start.";
				return;
			}
			if (!HasSession || State == Phase.Error)
			{
				OpenSession();
			}
			if (State != Phase.Lobby)
			{
				return;
			}
			startRequest.Cancel();
			if (Net.Members.Length > 150)
			{
				Status = "ScoreRace supports up to 150 participants (144 competitors).";
				plugin.LogMessage(Status);
				return;
			}
			if (!startRequest.Request(Net.Members.Count((string id) => !LobbySpectators.Contains(id)), Transport.Now))
			{
				Status = "Assign at least one competitor, then press START GAME again.";
				return;
			}
			Status = "Checking all player mods and configurations before starting.";
			Plugin.Log.LogInfo((object)("Native start requested: " + (solo ? "Singleplayer" : "Lobby Start")));
			if (solo && (Object)(object)MenuManager.instance != (Object)null)
			{
				MenuManager.instance.PageCloseAll();
			}
		}

		private void NativeLobbyTick(double now)
		{
			if (returningToLobby || State == Phase.Returning)
			{
				return;
			}
			bool inNativeLobby = GameBridge.InNativeLobby;
			if (inNativeLobby && !plugin.Game.Active)
			{
				wasNativeLobby = true;
				plugin.Game.CaptureNormalState();
				if (!HasSession && Host)
				{
					OpenSession();
				}
			}
			if (wasNativeLobby && !inNativeLobby && !plugin.Game.Active && !startRequest.Pending && State != Phase.Preparing)
			{
				wasNativeLobby = false;
				ClearSession();
				plugin.Game.Restore();
			}
			if (State != Phase.Lobby)
			{
				return;
			}
			if (HasSession && Net.LobbyId != sessionLobby)
			{
				startRequest.Cancel();
				ClearSession();
				return;
			}
			if (observerOffer.Length == 0 && now >= nextAutoReady && !Busy && !waitingReady && !ReadySelf && local != null && local.Verify() && HostSettings.Length > 0)
			{
				nextAutoReady = now + 1.0;
				ToggleReady();
			}
			if (startRequest.Expire(now))
			{
				Status = "Setup check incomplete. Match all mods and configurations, then press Start again.";
			}
			else if (startRequest.Take(CanStart, now))
			{
				if (inNativeLobby && (Object)(object)SteamManager.instance != (Object)null)
				{
					SteamManager.instance.LockLobby();
				}
				StartRound();
				if (State == Phase.Preparing && (Object)(object)MenuManager.instance != (Object)null)
				{
					MenuMana