Decompiled source of CharacterVault v1.0.29

Landoria.CharacterVault.dll

Decompiled 4 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Landoria.SharedLib;
using PartyCSharpSDK;
using PlayFab;
using PlayFab.Party;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("Landoria.CharacterVault")]
[assembly: AssemblyDescription("Stores trusted character profiles and world-scoped online activity on the server.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Landoria.CharacterVault")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("70BCF48B-1C23-4B2A-8A4F-F01CC21E3EC5")]
[assembly: AssemblyFileVersion("1.0.29")]
[assembly: AssemblyInformationalVersion("1.0.29")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = "")]
[assembly: AssemblyVersion("1.0.29.19303")]
namespace Landoria.CharacterVault
{
	internal sealed class CharacterVaultSettings
	{
		private bool serverInitialized;

		internal bool AllowMultipleCharacters { get; private set; } = true;

		internal IReadOnlyList<StartingItem> StartingItems { get; private set; }

		internal CharacterVaultSettings()
		{
			StartingItems = new List<StartingItem>();
		}

		internal void InitializeServer()
		{
			if (!serverInitialized && ServerRole.IsDedicatedServer)
			{
				string[] commandLineArgs = Environment.GetCommandLineArgs();
				AllowMultipleCharacters = CharacterVaultArgumentPolicy.ResolveAllowMultiple(commandLineArgs);
				StartingItems = ParseItems(CharacterVaultArgumentPolicy.ResolveStartingItems(commandLineArgs));
				serverInitialized = true;
				CharacterVaultPlugin.Log.LogInfo($"Server allowMultipleCharacters={AllowMultipleCharacters}, " + $"startingItemCount={StartingItems.Count}.");
			}
		}

		private static IReadOnlyList<StartingItem> ParseItems(string value)
		{
			List<StartingItem> list = new List<StartingItem>();
			if (string.IsNullOrWhiteSpace(value))
			{
				return list;
			}
			string[] array = value.Split(new char[1] { ',' });
			foreach (string text in array)
			{
				string[] array2 = text.Split(new char[1] { ':' });
				if (array2.Length != 2 || string.IsNullOrWhiteSpace(array2[0]) || !int.TryParse(array2[1].Trim(), out var result) || result <= 0)
				{
					throw new InvalidOperationException("Invalid CharacterVault starting item '" + text + "'.");
				}
				list.Add(new StartingItem(array2[0].Trim(), result));
			}
			return list;
		}
	}
	public interface ICharacterRestoreProvider
	{
		Task<CharacterRestoreResult> RestoreAsync(string platformPlayerId, string playerName, CancellationToken cancellationToken);
	}
	public sealed class CharacterRestoreResult
	{
		public CharacterRestoreStatus Status { get; }

		public byte[] Profile { get; }

		private CharacterRestoreResult(CharacterRestoreStatus status, byte[] profile)
		{
			Status = status;
			Profile = profile;
		}

		public static CharacterRestoreResult Restored(byte[] profile)
		{
			return new CharacterRestoreResult(CharacterRestoreStatus.Restored, profile ?? throw new ArgumentNullException("profile"));
		}

		public static CharacterRestoreResult NotFound()
		{
			return new CharacterRestoreResult(CharacterRestoreStatus.NotFound, null);
		}

		public static CharacterRestoreResult Failed()
		{
			return new CharacterRestoreResult(CharacterRestoreStatus.Failed, null);
		}
	}
	public enum CharacterRestoreStatus
	{
		Restored,
		NotFound,
		Failed
	}
	public static class CharacterRestoreApi
	{
		private static readonly object Sync = new object();

		private static ICharacterRestoreProvider _provider;

		public static void Register(ICharacterRestoreProvider provider)
		{
			if (provider == null)
			{
				throw new ArgumentNullException("provider");
			}
			lock (Sync)
			{
				if (_provider != null && _provider != provider)
				{
					throw new InvalidOperationException("A character restore provider is already registered.");
				}
				_provider = provider;
			}
		}

		public static bool Unregister(ICharacterRestoreProvider provider)
		{
			lock (Sync)
			{
				if (_provider != provider)
				{
					return false;
				}
				_provider = null;
				return true;
			}
		}

		internal static ICharacterRestoreProvider GetProvider()
		{
			lock (Sync)
			{
				return _provider;
			}
		}
	}
	internal static class CharacterVaultArgumentPolicy
	{
		private const string MultipleArgument = "--charactervault-allow-multiple-characters";

		private const string ItemsArgument = "--charactervault-starting-items";

		internal static bool ResolveAllowMultiple(string[] arguments)
		{
			if (!TryReadValue(arguments, "--charactervault-allow-multiple-characters", out var value))
			{
				return true;
			}
			if (!bool.TryParse(value, out var result))
			{
				throw new InvalidOperationException("Command-line switch --charactervault-allow-multiple-characters requires true or false.");
			}
			return result;
		}

		internal static string ResolveStartingItems(string[] arguments)
		{
			if (!TryReadValue(arguments, "--charactervault-starting-items", out var value))
			{
				return "";
			}
			return value;
		}

		private static bool TryReadValue(string[] arguments, string name, out string value)
		{
			value = "";
			bool flag = false;
			for (int i = 0; i < arguments.Length; i++)
			{
				if (string.Equals(arguments[i], name, StringComparison.OrdinalIgnoreCase))
				{
					if (flag || i + 1 >= arguments.Length)
					{
						throw new InvalidOperationException("Command-line switch " + name + " is missing or duplicated.");
					}
					value = arguments[++i];
					flag = true;
				}
			}
			return flag;
		}
	}
	internal interface ICharacterProfileCatalog
	{
		bool HasProfile(string accountId);
	}
	internal enum CharacterAdmission
	{
		ExistingProfile,
		NewEnrollment,
		RejectUnregisteredProfile,
		RejectAdditionalCharacter,
		RejectConcurrentEnrollment
	}
	internal static class CharacterAdmissionPolicy
	{
		internal static CharacterAdmission Decide(bool hasStoredProfile, bool createdThisSession, bool allowMultipleCharacters, bool accountHasProfile, bool enrollmentAvailable)
		{
			if (hasStoredProfile)
			{
				return CharacterAdmission.ExistingProfile;
			}
			if (!createdThisSession)
			{
				return CharacterAdmission.RejectUnregisteredProfile;
			}
			if (!allowMultipleCharacters && accountHasProfile)
			{
				return CharacterAdmission.RejectAdditionalCharacter;
			}
			if (!enrollmentAvailable)
			{
				return CharacterAdmission.RejectConcurrentEnrollment;
			}
			return CharacterAdmission.NewEnrollment;
		}
	}
	internal static class CharacterAdmissionMessages
	{
		internal static string ForRejection(CharacterAdmission admission, IReadOnlyList<string> existingProfileNames)
		{
			if (admission == CharacterAdmission.RejectAdditionalCharacter && existingProfileNames != null && existingProfileNames.Count > 0)
			{
				string text = string.Join(", ", existingProfileNames.Select((string name) => name.ToUpperInvariant()));
				if (existingProfileNames.Count != 1)
				{
					return "You already have characters: " + text + ". You cannot create more.";
				}
				return "You already have a character: " + text + ". You cannot create more.";
			}
			if (admission != CharacterAdmission.RejectUnregisteredProfile)
			{
				return "This platform account already has a character.";
			}
			if (existingProfileNames == null || existingProfileNames.Count == 0)
			{
				return "Create a new character before joining this server.";
			}
			return ((existingProfileNames.Count == 1) ? "Create a new character or use the previously used one: " : "Create a new character or use one of the previously used ones: ") + string.Join(", ", existingProfileNames.Select((string name) => name.ToUpperInvariant())) + ".";
		}
	}
	internal sealed class CharacterAdmissionEvaluator
	{
		private readonly ICharacterProfileCatalog _profiles;

		internal CharacterAdmissionEvaluator(ICharacterProfileCatalog profiles)
		{
			_profiles = profiles;
		}

		internal CharacterAdmission Decide(bool hasStoredProfile, string accountId, bool createdThisSession, bool allowMultipleCharacters, bool enrollmentAvailable)
		{
			bool accountHasProfile = !hasStoredProfile && createdThisSession && !allowMultipleCharacters && _profiles.HasProfile(accountId);
			return CharacterAdmissionPolicy.Decide(hasStoredProfile, createdThisSession, allowMultipleCharacters, accountHasProfile, enrollmentAvailable);
		}
	}
	internal sealed class ServerProfileSessionState
	{
		internal bool CanSave
		{
			get
			{
				if (Verified && Admitted)
				{
					return Permitted;
				}
				return false;
			}
		}

		internal bool PermissionChecked { get; private set; }

		internal bool Verified { get; set; }

		internal bool Admitted { get; set; }

		internal bool Permitted { get; private set; }

		internal void RecordPermission(bool permitted)
		{
			PermissionChecked = true;
			Permitted = permitted;
		}
	}
	internal static class SaveAcknowledgementPolicy
	{
		internal static bool CanAcknowledge(ServerProfileSessionState session)
		{
			return session?.CanSave ?? false;
		}
	}
	internal static class CharacterVaultRejection
	{
		internal const string MessageRpc = "CharacterVault_Rejection_v1";

		internal const string AckRpc = "CharacterVault_RejectionAck_v1";

		private const float DisconnectFallbackSeconds = 2f;

		private static readonly Dictionary<ZRpc, float> Deadlines = new Dictionary<ZRpc, float>();

		private static readonly HashSet<ZRpc> DisconnectRequested = new HashSet<ZRpc>();

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

		internal static void RegisterServer(ZRpc rpc)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Expected O, but got Unknown
			rpc.Register("CharacterVault_RejectionAck_v1", new Method(ReceiveAck));
		}

		internal static void RegisterClient(ZRpc rpc)
		{
			ClearClient();
			rpc.Register<ZPackage>("CharacterVault_Rejection_v1", (Action<ZRpc, ZPackage>)ReceiveMessage);
		}

		internal static void Reject(ZRpc rpc, string message, string systemMessage = null)
		{
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Expected O, but got Unknown
			CharacterVaultPlugin.Log.LogWarning("CharacterVault rejected " + rpc.GetSocket().GetHostName() + ": " + message);
			Deadlines[rpc] = Time.unscaledTime + 2f;
			ZPackage val = new ZPackage();
			val.Write(message);
			val.Write(systemMessage ?? message);
			rpc.Invoke("CharacterVault_Rejection_v1", new object[1] { val });
		}

		internal static void RecordPermittedListRejection(string hostName)
		{
			PermittedListRejections.Add(hostName);
		}

		internal static void SendPermittedListRejection(ZRpc rpc)
		{
			object obj;
			if (rpc == null)
			{
				obj = null;
			}
			else
			{
				ISocket socket = rpc.GetSocket();
				obj = ((socket != null) ? socket.GetHostName() : null);
			}
			string text = (string)obj;
			if (text != null && PermittedListRejections.Remove(text))
			{
				Reject(rpc, "This platform account is not allowed to join this server.");
			}
		}

		internal static void SetClientMessage(string userMessage, string systemMessage = null)
		{
			ConnectionFailureMessages.Push("Landoria.CharacterVault", userMessage, systemMessage);
		}

		internal static void ClearPendingClientMessage()
		{
			ClearClient();
		}

		internal static void Remove(ZRpc rpc)
		{
			Deadlines.Remove(rpc);
			DisconnectRequested.Remove(rpc);
		}

		internal static void Tick()
		{
			DisconnectExpired();
		}

		internal static void Clear()
		{
			Deadlines.Clear();
			DisconnectRequested.Clear();
			PermittedListRejections.Clear();
			ClearClient();
		}

		private static void ReceiveMessage(ZRpc rpc, ZPackage package)
		{
			string text = package.ReadString();
			string text2 = package.ReadString();
			ConnectionFailureMessages.Push("Landoria.CharacterVault", text, text2);
			CharacterVaultPlugin.Log.LogWarning("Server rejected the character: " + text + " System detail: " + text2);
			rpc.Invoke("CharacterVault_RejectionAck_v1", Array.Empty<object>());
			CharacterVaultPlugin.Log.LogDebug("Acknowledged the CharacterVault rejection; waiting for the server disconnect.");
		}

		private static void ReceiveAck(ZRpc rpc)
		{
			if (Deadlines.ContainsKey(rpc))
			{
				RequestDisconnect(rpc);
			}
		}

		private static void DisconnectExpired()
		{
			ZRpc[] array = (from entry in Deadlines
				where Time.unscaledTime >= entry.Value
				select entry.Key).ToArray();
			foreach (ZRpc val in array)
			{
				if (DisconnectRequested.Contains(val))
				{
					ForceDisconnect(val);
				}
				else
				{
					RequestDisconnect(val);
				}
			}
		}

		private static void RequestDisconnect(ZRpc rpc)
		{
			DisconnectRequested.Add(rpc);
			Deadlines[rpc] = Time.unscaledTime + 2f;
			CharacterVaultPlugin.Log.LogDebug("Requesting rejected pre-spawn client disconnection.");
			rpc.Invoke("Disconnect", Array.Empty<object>());
		}

		private static void ForceDisconnect(ZRpc rpc)
		{
			Deadlines.Remove(rpc);
			DisconnectRequested.Remove(rpc);
			ZNet instance = ZNet.instance;
			ZNetPeer val = ((instance != null) ? ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == rpc)) : null);
			if (val != null)
			{
				CharacterVaultPlugin.Log.LogWarning("Rejected client did not disconnect; closing the server connection.");
				ZNet.instance.Disconnect(val);
			}
		}

		private static void ClearClient()
		{
			ConnectionFailureMessages.Clear("Landoria.CharacterVault");
		}
	}
	internal static class CharacterRejectionMessages
	{
		internal const string PermittedListDenied = "This platform account is not allowed to join this server.";

		internal const string AdditionalCharacterDenied = "This platform account already has a character.";
	}
	internal sealed class CharacterSaveStatusDisplay
	{
		private const float ActiveDisplaySeconds = 30f;

		private const float CommitTimeoutSeconds = 20f;

		private const float ResultDisplaySeconds = 3f;

		private readonly SaveStatusLifecycle _lifecycle = new SaveStatusLifecycle();

		private TextMeshProUGUI _label;

		internal void Attach(Minimap minimap)
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_008c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fa: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
			if (!((Object)(object)_label != (Object)null) && !((Object)(object)minimap?.m_mapImageSmall == (Object)null) && !((Object)(object)minimap.m_biomeNameSmall == (Object)null))
			{
				GameObject val = new GameObject("CharacterVaultSaveStatus", new Type[1] { typeof(RectTransform) });
				val.transform.SetParent(minimap.m_smallRoot.transform, false);
				RectTransform component = val.GetComponent<RectTransform>();
				component.anchorMin = new Vector2(0.5f, 0.5f);
				component.anchorMax = new Vector2(0.5f, 0.5f);
				component.pivot = new Vector2(0.5f, 1f);
				component.sizeDelta = new Vector2(480f, 30f);
				RectTransform rectTransform = ((Graphic)minimap.m_mapImageSmall).rectTransform;
				Rect rect = rectTransform.rect;
				float x = ((Rect)(ref rect)).center.x;
				rect = rectTransform.rect;
				((Transform)component).position = ((Transform)rectTransform).TransformPoint(new Vector3(x, ((Rect)(ref rect)).yMin - 8f, 0f));
				((Transform)component).SetAsLastSibling();
				_label = val.AddComponent<TextMeshProUGUI>();
				((TMP_Text)_label).font = minimap.m_biomeNameSmall.font;
				((TMP_Text)_label).fontSharedMaterial = minimap.m_biomeNameSmall.fontSharedMaterial;
				((TMP_Text)_label).fontSize = minimap.m_biomeNameSmall.fontSize;
				((TMP_Text)_label).fontSizeMax = minimap.m_biomeNameSmall.fontSize;
				((TMP_Text)_label).fontSizeMin = 8f;
				((TMP_Text)_label).enableAutoSizing = true;
				((TMP_Text)_label).textWrappingMode = (TextWrappingModes)0;
				((TMP_Text)_label).fontStyle = (FontStyles)1;
				((TMP_Text)_label).alignment = (TextAlignmentOptions)514;
				((Graphic)_label).color = Color.white;
				((Graphic)_label).raycastTarget = false;
				((TMP_Text)_label).text = string.Empty;
				val.SetActive(false);
			}
		}

		internal void ShowSaving(string requestId)
		{
			Show(requestId, "Saving character...", 30f, waitingForCommit: false);
		}

		internal void ShowAccepted(string requestId)
		{
			int version = Show(requestId, "Saving character......", 30f, waitingForCommit: true);
			CharacterVaultPlugin.Instance?.Run(FailWithoutCommit(requestId, version));
		}

		internal void ShowCommitted(string requestId)
		{
			if (_lifecycle.CanCommit(requestId))
			{
				Show(requestId, "Character saved", 3f, waitingForCommit: false);
			}
		}

		internal void Hide()
		{
			_lifecycle.Clear();
			if ((Object)(object)_label != (Object)null)
			{
				((Component)_label).gameObject.SetActive(false);
			}
		}

		internal void Dispose()
		{
			_lifecycle.Clear();
			if ((Object)(object)_label != (Object)null)
			{
				Object.Destroy((Object)(object)((Component)_label).gameObject);
				_label = null;
			}
		}

		private int Show(string requestId, string message, float duration, bool waitingForCommit)
		{
			int num = _lifecycle.Begin(requestId, waitingForCommit);
			Attach(Minimap.instance);
			if ((Object)(object)_label == (Object)null)
			{
				return num;
			}
			((TMP_Text)_label).text = message;
			((Component)_label).gameObject.SetActive(true);
			CharacterVaultPlugin.Instance?.Run(HideAfterDelay(num, duration));
			return num;
		}

		private IEnumerator FailWithoutCommit(string requestId, int version)
		{
			yield return (object)new WaitForSecondsRealtime(20f);
			if (_lifecycle.CanFail(requestId, version))
			{
				Show(requestId, "Failed", 3f, waitingForCommit: false);
			}
		}

		private IEnumerator HideAfterDelay(int version, float duration)
		{
			yield return (object)new WaitForSecondsRealtime(duration);
			if (_lifecycle.IsCurrent(version))
			{
				Hide();
			}
		}
	}
	internal sealed class ClientSaveLifecycle
	{
		private bool _active;

		private bool _enrolling;

		private bool _spawned;

		internal bool IsActive => _active;

		internal bool CanUpload
		{
			get
			{
				if (_active)
				{
					return _spawned;
				}
				return false;
			}
		}

		internal bool IsEnrolling => _enrolling;

		internal bool HasSpawned => _spawned;

		internal void ActivateExisting()
		{
			_active = true;
		}

		internal void BeginEnrollment()
		{
			_active = true;
			_enrolling = true;
		}

		internal bool RecordSpawn(bool isLocalPlayer)
		{
			if (!isLocalPlayer)
			{
				return false;
			}
			_spawned = true;
			bool enrolling = _enrolling;
			_enrolling = false;
			return enrolling;
		}

		internal void Reset()
		{
			_active = false;
			_enrolling = false;
			_spawned = false;
		}
	}
	internal sealed class ServerFinalSaveMonitor
	{
		private sealed class PendingDisconnect
		{
			internal float Deadline { get; }

			internal string PlayerName { get; }

			internal PendingDisconnect(string playerName, float deadline)
			{
				PlayerName = playerName;
				Deadline = deadline;
			}
		}

		private const float TimeoutSeconds = 10f;

		private readonly Dictionary<ZRpc, PendingDisconnect> _pending = new Dictionary<ZRpc, PendingDisconnect>();

		private readonly HashSet<ZRpc> _receivedFinalSaves = new HashSet<ZRpc>();

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

		internal void Observe(ZRpc rpc, string playerName, bool connected)
		{
			if (rpc != null)
			{
				if (connected)
				{
					_pending.Remove(rpc);
					_warned.Remove(rpc);
				}
				else if (!_receivedFinalSaves.Contains(rpc) && !_pending.ContainsKey(rpc) && !_warned.Contains(rpc))
				{
					_pending[rpc] = new PendingDisconnect(playerName, Time.realtimeSinceStartup + 10f);
				}
			}
		}

		internal void RecordSaveReceived(ZRpc rpc, string requestId)
		{
			if (IsFinalDisconnectRequest(requestId))
			{
				_receivedFinalSaves.Add(rpc);
				_pending.Remove(rpc);
				_warned.Remove(rpc);
			}
		}

		private static bool IsFinalDisconnectRequest(string requestId)
		{
			if (requestId == null || !requestId.StartsWith("disconnect-", StringComparison.Ordinal))
			{
				return requestId?.StartsWith("server-disconnect-", StringComparison.Ordinal) ?? false;
			}
			return true;
		}

		internal void Update()
		{
			foreach (ZRpc item in new List<ZRpc>(_pending.Keys))
			{
				PendingDisconnect pendingDisconnect = _pending[item];
				if (!(Time.realtimeSinceStartup < pendingDisconnect.Deadline))
				{
					_pending.Remove(item);
					_warned.Add(item);
					CharacterVaultPlugin.Log.LogWarning("No final character save was received from " + pendingDisconnect.PlayerName + " " + $"within {10f:0} seconds after the connection was lost.");
				}
			}
		}

		internal void RecordRemoved(ZRpc rpc, string playerName)
		{
			Observe(rpc, playerName, connected: false);
			_receivedFinalSaves.Remove(rpc);
		}

		internal void Clear()
		{
			_pending.Clear();
			_receivedFinalSaves.Clear();
			_warned.Clear();
		}
	}
	internal static class NewCharacterPolicy
	{
		internal static bool HasNeverJoinedAWorld(PlayerProfile profile)
		{
			if (profile.m_firstSpawn)
			{
				return profile.m_playerStats[0].m_knownWorlds.Count == 0;
			}
			return false;
		}
	}
	internal static class ProfileFile
	{
		internal static byte[] Read(PlayerProfile profile)
		{
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Expected O, but got Unknown
			FileReader val = new FileReader(profile.GetPath(), profile.m_fileSource, (FileHelperType)0);
			try
			{
				Stream baseStream = val.m_binary.BaseStream;
				baseStream.Position = 0L;
				byte[] array = new byte[baseStream.Length];
				int num;
				for (int i = 0; i < array.Length; i += num)
				{
					num = baseStream.Read(array, i, array.Length - i);
					if (num == 0)
					{
						throw new EndOfStreamException("The character profile ended unexpectedly.");
					}
				}
				return array;
			}
			finally
			{
				val.Dispose();
			}
		}

		internal static PlayerProfile ReplaceSelected(byte[] data)
		{
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//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_0060: Expected O, but got Unknown
			PlayerProfile playerProfile = Game.instance.GetPlayerProfile();
			string path = playerProfile.GetPath();
			string text = path + ".vault-new";
			Write(text, playerProfile.m_fileSource, data);
			SaveApiCompatibility.ReplaceOldFile(path, text, playerProfile.m_fileSource);
			SaveApiCompatibility.InvalidateCharacterCache();
			PlayerProfile val = new PlayerProfile(playerProfile.GetFilename(), playerProfile.m_fileSource);
			if (!val.Load())
			{
				throw new InvalidDataException("Valheim rejected the authoritative server profile.");
			}
			return val;
		}

		private static void Write(string path, FileSource source, byte[] data)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001f: Invalid comparison between Unknown and I4
			FileWriter obj = SaveApiCompatibility.CreateWriter(path, source);
			obj.m_binary.Write(data);
			obj.Finish();
			if ((int)obj.Status != 2)
			{
				throw new IOException("The authoritative character profile could not be written.");
			}
		}
	}
	internal static class SaveApiCompatibility
	{
		private static readonly Type[] LegacyWriterParameters = new Type[3]
		{
			typeof(string),
			typeof(FileHelperType),
			typeof(FileSource)
		};

		private static readonly Type[] CurrentWriterParameters = new Type[4]
		{
			typeof(string),
			typeof(CloudStorageFileGrouping),
			typeof(FileHelperType),
			typeof(FileSource)
		};

		internal static FileSource LocalSource => (FileSource)Enum.Parse(typeof(FileSource), "Local");

		internal static FileWriter CreateWriter(string path, FileSource source)
		{
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			//IL_0098: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00aa: Expected O, but got Unknown
			ConstructorInfo constructor = typeof(FileWriter).GetConstructor(CurrentWriterParameters);
			if (!(constructor != null))
			{
				constructor = typeof(FileWriter).GetConstructor(LegacyWriterParameters) ?? throw new MissingMethodException(typeof(FileWriter).FullName, ".ctor");
				return (FileWriter)constructor.Invoke(new object[3]
				{
					path,
					(object)(FileHelperType)0,
					source
				});
			}
			return (FileWriter)constructor.Invoke(new object[4]
			{
				path,
				(object)(CloudStorageFileGrouping)0,
				(object)(FileHelperType)0,
				source
			});
		}

		internal static void ReplaceOldFile(string path, string next, FileSource source)
		{
			//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			MethodInfo methodInfo = FindMethod(typeof(FileHelpers), "ReplaceOldFile", typeof(string), typeof(string), typeof(string), typeof(CloudStorageFileGrouping), typeof(FileSource));
			object[] parameters = ((!(methodInfo != null)) ? new object[4]
			{
				path,
				next,
				path + ".old",
				source
			} : new object[5]
			{
				path,
				next,
				path + ".old",
				(object)(CloudStorageFileGrouping)0,
				source
			});
			(methodInfo ?? RequireMethod(typeof(FileHelpers), "ReplaceOldFile", typeof(string), typeof(string), typeof(string), typeof(FileSource))).Invoke(null, parameters);
		}

		internal static void InvalidateCharacterCache()
		{
			MethodInfo methodInfo = FindMethod(typeof(SaveSystem), "InvalidateCache", typeof(SaveDataType));
			if (methodInfo != null)
			{
				methodInfo.Invoke(null, new object[1] { (object)(SaveDataType)1 });
			}
			else
			{
				RequireMethod(typeof(SaveSystem), "InvalidateCache").Invoke(null, null);
			}
		}

		internal static string GetCharacterPath(FileSource source, string filename)
		{
			//IL_009b: 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)
			MethodInfo methodInfo = FindMethod(typeof(SaveSystem), "GetCharacterPath", typeof(FileSource), typeof(string));
			if (methodInfo != null)
			{
				return (string)methodInfo.Invoke(null, new object[2] { source, filename });
			}
			return (string)RequireMethod(typeof(PlayerProfile), "GetPath", typeof(FileSource), typeof(string)).Invoke(null, new object[2] { source, filename });
		}

		private static MethodInfo FindMethod(Type type, string name, params Type[] parameters)
		{
			return type.GetMethod(name, BindingFlags.Static | BindingFlags.Public, null, parameters, null);
		}

		private static MethodInfo RequireMethod(Type type, string name, params Type[] parameters)
		{
			return FindMethod(type, name, parameters) ?? throw new MissingMethodException(type.FullName, name);
		}
	}
	internal static class SaveStatusMessages
	{
		internal const string Saving = "Saving character...";

		internal const string Accepted = "Saving character......";

		internal const string Saved = "Character saved";

		internal const string Failed = "Failed";
	}
	internal sealed class SaveStatusLifecycle
	{
		private string _requestId = string.Empty;

		private bool _waitingForCommit;

		internal int Version { get; private set; }

		internal int Begin(string requestId, bool waitingForCommit)
		{
			Version++;
			_requestId = requestId;
			_waitingForCommit = waitingForCommit;
			return Version;
		}

		internal bool CanCommit(string requestId)
		{
			if (_requestId == requestId)
			{
				return _waitingForCommit;
			}
			return false;
		}

		internal bool CanFail(string requestId, int version)
		{
			if (Version == version && _requestId == requestId)
			{
				return _waitingForCommit;
			}
			return false;
		}

		internal bool IsCurrent(int version)
		{
			return Version == version;
		}

		internal void Clear()
		{
			Version++;
			_requestId = string.Empty;
			_waitingForCommit = false;
		}
	}
	internal sealed class VaultSession
	{
		internal string AccountId { get; }

		internal long CharacterId { get; }

		internal string Name { get; }

		internal bool NewCharacter { get; }

		internal ServerProfileSessionState State { get; } = new ServerProfileSessionState();

		internal bool Enrolling { get; set; }

		internal VaultSession(string accountId, long characterId, string name, bool newCharacter)
		{
			AccountId = accountId;
			CharacterId = characterId;
			Name = name;
			NewCharacter = newCharacter;
		}
	}
	internal sealed class PendingCommit
	{
		internal byte[] Data { get; }

		internal ZRpc Rpc { get; }

		internal string RequestId { get; }

		internal VaultSession Session { get; }

		internal PendingCommit(ZRpc rpc, VaultSession session, string requestId, byte[] data)
		{
			Rpc = rpc;
			Session = session;
			RequestId = requestId;
			Data = data;
		}
	}
	internal sealed class ProfileCommitQueue
	{
		private readonly Queue<PendingCommit> _commits = new Queue<PendingCommit>();

		private readonly object _lock = new object();

		private readonly VaultStorage _storage;

		private readonly SynchronizationContext _unityContext;

		private readonly Action<PendingCommit> _confirm;

		private bool _workerRunning;

		internal ProfileCommitQueue(VaultStorage storage, SynchronizationContext unityContext, Action<PendingCommit> confirm)
		{
			_storage = storage ?? throw new ArgumentNullException("storage");
			_unityContext = unityContext ?? throw new ArgumentNullException("unityContext");
			_confirm = confirm ?? throw new ArgumentNullException("confirm");
		}

		internal void Enqueue(PendingCommit commit)
		{
			lock (_lock)
			{
				_commits.Enqueue(commit);
				if (!_workerRunning)
				{
					_workerRunning = true;
					ThreadPool.QueueUserWorkItem(delegate
					{
						Process();
					});
				}
			}
		}

		private void Process()
		{
			while (true)
			{
				if (!TryDequeue(out var commit))
				{
					break;
				}
				try
				{
					_storage.Commit(commit.Session.AccountId, commit.Session.Name, commit.Data);
					_unityContext.Post(delegate
					{
						_confirm(commit);
					}, null);
				}
				catch (Exception arg)
				{
					CharacterVaultPlugin.Log.LogError($"Character vault commit failed: {arg}");
				}
			}
		}

		private bool TryDequeue(out PendingCommit commit)
		{
			lock (_lock)
			{
				commit = ((_commits.Count > 0) ? _commits.Dequeue() : null);
				if (commit == null)
				{
					_workerRunning = false;
				}
				return commit != null;
			}
		}
	}
	internal static class ProfileTransferProtocol
	{
		internal const int ChunkSize = 65536;

		internal static ZPackage Begin(string transferId, int length, string hash)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write(transferId);
			val.Write(length);
			val.Write(hash);
			return val;
		}

		internal static ZPackage Chunk(string transferId, byte[] data, int offset)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_003c: Expected O, but got Unknown
			int num = Math.Min(65536, data.Length - offset);
			byte[] array = new byte[num];
			Buffer.BlockCopy(data, offset, array, 0, num);
			ZPackage val = new ZPackage();
			val.Write(transferId);
			val.Write(offset);
			val.Write(array);
			return val;
		}
	}
	internal sealed class IncomingTransfer
	{
		private readonly byte[] _data;

		private readonly bool[] _blocks;

		private readonly string _hash;

		private readonly string _transferId;

		internal string RequestId { get; set; }

		private IncomingTransfer(string transferId, int length, string hash)
		{
			_transferId = transferId;
			_hash = hash;
			_data = new byte[length];
			_blocks = new bool[(length + 65536 - 1) / 65536];
		}

		internal static IncomingTransfer Create(ZPackage package, int maximumLength)
		{
			string text = package.ReadString();
			int num = package.ReadInt();
			string text2 = package.ReadString();
			if (string.IsNullOrWhiteSpace(text) || num <= 0 || num > maximumLength || text2.Length != 64)
			{
				throw new InvalidDataException("The profile transfer header is invalid.");
			}
			return new IncomingTransfer(text, num, text2);
		}

		internal void Add(ZPackage package)
		{
			string text = package.ReadString();
			int num = package.ReadInt();
			byte[] array = package.ReadByteArray();
			if (text != _transferId || num < 0 || num % 65536 != 0 || array.Length == 0 || array.Length > 65536 || num + array.Length > _data.Length)
			{
				throw new InvalidDataException("The profile transfer chunk is invalid.");
			}
			int num2 = num / 65536;
			if (_blocks[num2])
			{
				throw new InvalidDataException("The profile transfer contains a duplicate chunk.");
			}
			Buffer.BlockCopy(array, 0, _data, num, array.Length);
			_blocks[num2] = true;
		}

		internal byte[] Complete(string transferId)
		{
			if (transferId != _transferId || _blocks.Any((bool block) => !block) || !string.Equals(VaultStorage.Hash(_data), _hash, StringComparison.OrdinalIgnoreCase))
			{
				throw new InvalidDataException("The profile transfer is incomplete or corrupted.");
			}
			return _data;
		}
	}
	internal sealed class ProfileTransferService : IDisposable
	{
		internal const string HelloRpc = "CharacterVault_Hello_v1";

		internal const string AdmissionRpc = "CharacterVault_Admission_v1";

		internal const string DownloadBeginRpc = "CharacterVault_DownloadBegin_v1";

		internal const string DownloadChunkRpc = "CharacterVault_DownloadChunk_v1";

		internal const string DownloadCompleteRpc = "CharacterVault_DownloadComplete_v1";

		internal const string UploadBeginRpc = "CharacterVault_UploadBegin_v1";

		internal const string UploadChunkRpc = "CharacterVault_UploadChunk_v1";

		internal const string UploadCompleteRpc = "CharacterVault_UploadComplete_v1";

		internal const string SaveRequestRpc = "CharacterVault_SaveRequest_v1";

		internal const string SaveAckRpc = "CharacterVault_SaveAck_v1";

		internal const string CommitAckRpc = "CharacterVault_CommitAck_v1";

		private const int MaximumProfileBytes = 67108864;

		private readonly Dictionary<ZRpc, VaultSession> _sessions = new Dictionary<ZRpc, VaultSession>();

		private readonly Dictionary<ZRpc, IncomingTransfer> _uploads = new Dictionary<ZRpc, IncomingTransfer>();

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

		private readonly ClientSaveLifecycle _clientLifecycle = new ClientSaveLifecycle();

		private readonly VaultStorage _storage = new VaultStorage();

		private readonly CharacterAdmissionEvaluator _admission;

		private readonly ProfileCommitQueue _commits;

		private readonly ServerFinalSaveMonitor _finalSaveMonitor = new ServerFinalSaveMonitor();

		private IncomingTransfer _download;

		private bool _clientUploadBusy;

		private bool _suppressNextClientUpload;

		private string _pendingRequest;

		private PlayerProfile _pendingProfile;

		private IReadOnlyList<StartingItem> _serverStartingItems = Array.Empty<StartingItem>();

		internal ProfileTransferService(SynchronizationContext unityContext)
		{
			if (unityContext == null)
			{
				throw new ArgumentNullException("unityContext");
			}
			_admission = new CharacterAdmissionEvaluator(_storage);
			_commits = new ProfileCommitQueue(_storage, unityContext, ConfirmBackgroundCommit);
		}

		internal void Register(ZNet network, ZNetPeer peer)
		{
			if (network.IsServer())
			{
				RegisterServer(peer.m_rpc);
			}
			else
			{
				RegisterClient(peer.m_rpc);
			}
		}

		internal void SendHello(ZRpc serverRpc)
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_001c: Expected O, but got Unknown
			Game instance = Game.instance;
			PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null);
			if (val != null)
			{
				ZPackage val2 = new ZPackage();
				val2.Write(val.GetPlayerID());
				val2.Write(val.GetName());
				val2.Write(NewCharacterPolicy.HasNeverJoinedAWorld(val));
				serverRpc.Invoke("CharacterVault_Hello_v1", new object[1] { val2 });
			}
		}

		internal bool Approve(ZRpc rpc)
		{
			if (!_sessions.TryGetValue(rpc, out var value))
			{
				Reject(rpc, "Character verification did not complete. Please try again.");
				return false;
			}
			if (value.State.Verified)
			{
				return value.State.Admitted;
			}
			value.State.Verified = true;
			if (_storage.TryRead(value.AccountId, value.Name, out var data))
			{
				SendDownload(rpc, value, data);
				value.State.Admitted = true;
				return true;
			}
			CharacterRestoreResult characterRestoreResult = TryRestore(value);
			if (characterRestoreResult != null && characterRestoreResult.Status == CharacterRestoreStatus.Restored)
			{
				if (!ValidateProfile(rpc, value, characterRestoreResult.Profile))
				{
					return false;
				}
				_storage.Commit(value.AccountId, value.Name, characterRestoreResult.Profile);
				SendDownload(rpc, value, characterRestoreResult.Profile);
				value.State.Admitted = true;
				return true;
			}
			if (characterRestoreResult != null && characterRestoreResult.Status == CharacterRestoreStatus.Failed)
			{
				_sessions.Remove(rpc);
				Reject(rpc, "Your saved character could not be restored right now. Please try again in a moment.");
				return false;
			}
			value.State.Admitted = AdmitEnrollment(rpc, value);
			return value.State.Admitted;
		}

		internal void RecordPermission(string hostName, bool permitted)
		{
			foreach (VaultSession item in _sessions.Values.Where((VaultSession candidate) => string.Equals(candidate.AccountId, hostName, StringComparison.Ordinal)))
			{
				item.State.RecordPermission(permitted);
			}
		}

		internal void Remove(ZNetPeer peer)
		{
			if (peer?.m_rpc != null)
			{
				if (_sessions.TryGetValue(peer.m_rpc, out var value))
				{
					_finalSaveMonitor.RecordRemoved(peer.m_rpc, value.Name);
				}
				_sessions.Remove(peer.m_rpc);
				_uploads.Remove(peer.m_rpc);
				ReleaseEnrollment(peer.m_rpc);
				CharacterVaultPlugin.ServerDisconnects?.RecordDisconnected(peer.m_rpc);
				ZNet instance = ZNet.instance;
				if (instance != null && !instance.IsServer())
				{
					ResetClientState();
				}
			}
		}

		internal string DescribeDisconnect(ZNetPeer peer, bool server)
		{
			ZRpc val = peer?.m_rpc;
			VaultSession value = null;
			bool flag = val != null && _sessions.TryGetValue(val, out value);
			ServerProfileSessionState serverProfileSessionState = (flag ? value.State : null);
			bool flag2 = ((!server) ? (CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false) : (CharacterVaultPlugin.ServerDisconnects?.HasPendingSave(val) ?? false));
			return string.Format("side={0}, peerReady={1}, ", server ? "server" : "client", peer != null && peer.IsReady()) + $"sessionTracked={flag}, verified={serverProfileSessionState?.Verified ?? false}, " + $"admitted={serverProfileSessionState?.Admitted ?? false}, permissionChecked={serverProfileSessionState?.PermissionChecked ?? false}, " + $"permitted={serverProfileSessionState?.Permitted ?? false}, canSave={serverProfileSessionState?.CanSave ?? false}, " + $"clientActive={_clientLifecycle.IsActive}, enrolling={_clientLifecycle.IsEnrolling}, " + $"spawned={_clientLifecycle.HasSpawned}, uploadBusy={_clientUploadBusy}, " + $"incomingUpload={val != null && _uploads.ContainsKey(val)}, " + $"incomingDownload={_download != null}, pendingSave={flag2}";
		}

		private static CharacterRestoreResult TryRestore(VaultSession session)
		{
			ICharacterRestoreProvider provider = CharacterRestoreApi.GetProvider();
			if (provider == null)
			{
				return null;
			}
			using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10.0));
			try
			{
				CharacterRestoreResult result = provider.RestoreAsync(session.AccountId, session.Name, cancellationTokenSource.Token).GetAwaiter().GetResult();
				if (result != null && result.Status == CharacterRestoreStatus.Restored && (result.Profile == null || result.Profile.Length == 0 || result.Profile.Length > 67108864))
				{
					return CharacterRestoreResult.Failed();
				}
				return result ?? CharacterRestoreResult.Failed();
			}
			catch (Exception ex)
			{
				CharacterVaultPlugin.Log.LogWarning("Character restore failed: " + ex);
				return CharacterRestoreResult.Failed();
			}
		}

		internal void MonitorFinalSaves()
		{
			ZNet instance = ZNet.instance;
			if (instance == null || !instance.IsServer())
			{
				return;
			}
			foreach (KeyValuePair<ZRpc, VaultSession> session in _sessions)
			{
				if (session.Value.State.CanSave)
				{
					ServerFinalSaveMonitor finalSaveMonitor = _finalSaveMonitor;
					ZRpc key = session.Key;
					string name = session.Value.Name;
					ISocket socket = session.Key.GetSocket();
					finalSaveMonitor.Observe(key, name, socket != null && socket.IsConnected());
				}
			}
			_finalSaveMonitor.Update();
		}

		internal void RequestWorldCheckpoint()
		{
			ZNet instance = ZNet.instance;
			if (instance == null || !instance.IsServer())
			{
				return;
			}
			string requestId = "world-" + Guid.NewGuid().ToString("N");
			foreach (ZNetPeer item in ZNet.instance.GetPeers().Where(IsReady))
			{
				RequestSave(item, requestId);
			}
		}

		internal void RequestSave(ZNetPeer peer, string requestId)
		{
			if (peer?.m_rpc != null && CanSave(_sessions, peer.m_rpc))
			{
				peer.m_rpc.Invoke("CharacterVault_SaveRequest_v1", new object[1] { requestId });
			}
		}

		internal bool CanRequestSave(ZNetPeer peer)
		{
			if (peer?.m_rpc != null)
			{
				return CanSave(_sessions, peer.m_rpc);
			}
			return false;
		}

		internal KickSaveEligibility GetKickSaveEligibility(ZNetPeer peer)
		{
			if (peer?.m_rpc != null && (!peer.IsReady() || ((ZDOID)(ref peer.m_characterID)).IsNone()))
			{
				return KickSaveEligibility.Rejected;
			}
			if (peer?.m_rpc == null || !_sessions.TryGetValue(peer.m_rpc, out var value))
			{
				return KickSaveEligibility.Unmanaged;
			}
			if (value.State.CanSave)
			{
				return KickSaveEligibility.SaveRequired;
			}
			if (!value.State.Verified || !value.State.Admitted || !value.State.PermissionChecked || value.State.Permitted)
			{
				return KickSaveEligibility.Unmanaged;
			}
			return KickSaveEligibility.Rejected;
		}

		internal bool SaveManualClientProfile()
		{
			if (_clientLifecycle.IsActive)
			{
				ZNet instance = ZNet.instance;
				if (instance != null && !instance.IsServer() && !((Object)(object)Game.instance == (Object)null))
				{
					Game.instance.SavePlayerProfile(true, false);
					return true;
				}
			}
			return false;
		}

		internal void UploadSavedProfile(PlayerProfile profile)
		{
			if (_suppressNextClientUpload)
			{
				_suppressNextClientUpload = false;
				CharacterVaultPlugin.Log.LogInfo("Skipped the redundant local save upload after a confirmed voluntary disconnect save.");
			}
			else
			{
				if (!_clientLifecycle.IsActive)
				{
					return;
				}
				ZNet instance = ZNet.instance;
				if (instance == null || instance.IsServer())
				{
					return;
				}
				if (!_clientLifecycle.CanUpload)
				{
					CharacterVaultPlugin.Log.LogInfo("Skipped the server upload for a local save before Player.OnSpawned completed.");
					return;
				}
				ZRpc serverRPC = ZNet.instance.GetServerRPC();
				if (serverRPC == null)
				{
					ResetClientState();
					return;
				}
				string text = _pendingRequest ?? ("save-" + Guid.NewGuid().ToString("N"));
				_pendingRequest = null;
				if (_clientUploadBusy)
				{
					_pendingRequest = text;
					CharacterVaultPlugin.Log.LogInfo("Queued character save request " + text + " while another upload is awaiting confirmation.");
					return;
				}
				byte[] data = ProfileFile.Read(profile);
				_clientUploadBusy = true;
				CharacterVaultPlugin.SaveStatus?.ShowSaving(text);
				CharacterVaultPlugin.Log.LogInfo("Uploading character profile " + profile.GetName() + " for save request " + text + ".");
				CharacterVaultPlugin.Instance.Run(SendUpload(serverRPC, profile, data, text));
			}
		}

		internal bool BeginFinalDisconnectSave(string requestId)
		{
			Game instance = Game.instance;
			PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null);
			if (_clientLifecycle.IsActive)
			{
				ZNet instance2 = ZNet.instance;
				if (instance2 != null && !instance2.IsServer() && val != null)
				{
					_pendingRequest = requestId;
					if (_clientUploadBusy)
					{
						CharacterVaultPlugin.Log.LogInfo("Final save request " + requestId + " is waiting for the active upload to finish.");
						return true;
					}
					CharacterVaultPlugin.Log.LogInfo("Writing the final local profile for " + val.GetName() + " before disconnect.");
					Game.instance.SavePlayerProfile(true, false);
					return true;
				}
			}
			return false;
		}

		internal void SuppressRedundantDisconnectUpload()
		{
			_suppressNextClientUpload = true;
		}

		internal void RecordPlayerSpawned(Player player)
		{
			if (!((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer))
			{
				StartingItemGrantPolicy.ApplyEnrollment(_clientLifecycle, isLocalPlayer: true, _serverStartingItems, FindItem, (GameObject prefab, int quantity) => ((Humanoid)player).GetInventory().AddItem(prefab, quantity), delegate
				{
					Game.instance.SavePlayerProfile(true, false);
				}, delegate(StartingItem item)
				{
					CharacterVaultPlugin.Log.LogError($"Could not grant starting item {item.Prefab}:{item.Quantity}.");
				});
			}
		}

		internal void ApplyPendingProfile(ref PlayerProfile profile)
		{
			if (_pendingProfile != null)
			{
				profile = _pendingProfile;
				_pendingProfile = null;
			}
		}

		public void Dispose()
		{
			_sessions.Clear();
			_uploads.Clear();
			_enrollments.Clear();
			_finalSaveMonitor.Clear();
			_download = null;
		}

		private void RegisterServer(ZRpc rpc)
		{
			CharacterVaultPlugin.Settings.InitializeServer();
			CharacterVaultRejection.RegisterServer(rpc);
			rpc.Register<ZPackage>("CharacterVault_Hello_v1", (Action<ZRpc, ZPackage>)ReceiveHello);
			rpc.Register<ZPackage>("CharacterVault_UploadBegin_v1", (Action<ZRpc, ZPackage>)ReceiveUploadBegin);
			rpc.Register<ZPackage>("CharacterVault_UploadChunk_v1", (Action<ZRpc, ZPackage>)ReceiveUploadChunk);
			rpc.Register<ZPackage>("CharacterVault_UploadComplete_v1", (Action<ZRpc, ZPackage>)ReceiveUploadComplete);
		}

		private void RegisterClient(ZRpc rpc)
		{
			ResetClientState();
			CharacterVaultPlugin.DisconnectCoordinator?.RecordConnectionStarted();
			CharacterVaultRejection.RegisterClient(rpc);
			rpc.Register<ZPackage>("CharacterVault_Admission_v1", (Action<ZRpc, ZPackage>)ReceiveAdmission);
			rpc.Register<ZPackage>("CharacterVault_DownloadBegin_v1", (Action<ZRpc, ZPackage>)ReceiveDownloadBegin);
			rpc.Register<ZPackage>("CharacterVault_DownloadChunk_v1", (Action<ZRpc, ZPackage>)ReceiveDownloadChunk);
			rpc.Register<ZPackage>("CharacterVault_DownloadComplete_v1", (Action<ZRpc, ZPackage>)ReceiveDownloadComplete);
			rpc.Register<string>("CharacterVault_SaveRequest_v1", (Action<ZRpc, string>)ReceiveSaveRequest);
			rpc.Register<string>("CharacterVault_SaveAck_v1", (Action<ZRpc, string>)ReceiveSaveAck);
			rpc.Register<string>("CharacterVault_CommitAck_v1", (Action<ZRpc, string>)ReceiveCommitAck);
		}

		private void ReceiveHello(ZRpc rpc, ZPackage package)
		{
			long characterId = package.ReadLong();
			string name = package.ReadString();
			bool newCharacter = package.ReadBool();
			string hostName = rpc.GetSocket().GetHostName();
			_sessions[rpc] = new VaultSession(hostName, characterId, name, newCharacter);
		}

		private bool AdmitEnrollment(ZRpc rpc, VaultSession session)
		{
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Expected O, but got Unknown
			bool allowMultipleCharacters = CharacterVaultPlugin.Settings.AllowMultipleCharacters;
			CharacterAdmission characterAdmission = _admission.Decide(hasStoredProfile: false, session.AccountId, session.NewCharacter, allowMultipleCharacters, enrollmentAvailable: true);
			if (characterAdmission == CharacterAdmission.NewEnrollment && !ReserveEnrollment(rpc, session))
			{
				characterAdmission = CharacterAdmission.RejectConcurrentEnrollment;
			}
			if (characterAdmission != CharacterAdmission.NewEnrollment)
			{
				_sessions.Remove(rpc);
				Reject(rpc, CharacterAdmissionMessages.ForRejection(characterAdmission, _storage.GetProfileNames(session.AccountId)));
				return false;
			}
			session.Enrolling = true;
			ZPackage val = new ZPackage();
			val.Write(session.CharacterId);
			val.Write(CharacterVaultPlugin.Settings.StartingItems.Count);
			foreach (StartingItem startingItem in CharacterVaultPlugin.Settings.StartingItems)
			{
				val.Write(startingItem.Prefab);
				val.Write(startingItem.Quantity);
			}
			rpc.Invoke("CharacterVault_Admission_v1", new object[1] { val });
			return true;
		}

		private void SendDownload(ZRpc rpc, VaultSession session, byte[] data)
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0071: Expected O, but got Unknown
			string text = Guid.NewGuid().ToString("N");
			string hash = VaultStorage.Hash(data);
			rpc.Invoke("CharacterVault_DownloadBegin_v1", new object[1] { ProfileTransferProtocol.Begin(text, data.Length, hash) });
			for (int i = 0; i < data.Length; i += 65536)
			{
				rpc.Invoke("CharacterVault_DownloadChunk_v1", new object[1] { ProfileTransferProtocol.Chunk(text, data, i) });
			}
			ZPackage val = new ZPackage();
			val.Write(text);
			rpc.Invoke("CharacterVault_DownloadComplete_v1", new object[1] { val });
		}

		private void ReceiveAdmission(ZRpc rpc, ZPackage package)
		{
			long num = package.ReadLong();
			if (Game.instance.GetPlayerProfile().GetPlayerID() != num)
			{
				throw new InvalidDataException("The server admitted a different character.");
			}
			int num2 = package.ReadInt();
			List<StartingItem> list = new List<StartingItem>(num2);
			for (int i = 0; i < num2; i++)
			{
				list.Add(new StartingItem(package.ReadString(), package.ReadInt()));
			}
			_serverStartingItems = list;
			_clientLifecycle.BeginEnrollment();
		}

		private void ReceiveDownloadBegin(ZRpc rpc, ZPackage package)
		{
			_download = IncomingTransfer.Create(package, 67108864);
		}

		private void ReceiveDownloadChunk(ZRpc rpc, ZPackage package)
		{
			_download?.Add(package);
		}

		private void ReceiveDownloadComplete(ZRpc rpc, ZPackage package)
		{
			string transferId = package.ReadString();
			byte[] array = _download?.Complete(transferId);
			_download = null;
			if (array == null)
			{
				throw new InvalidDataException("The authoritative profile transfer was incomplete.");
			}
			_pendingProfile = ProfileFile.ReplaceSelected(array);
			_clientLifecycle.ActivateExisting();
		}

		private void ReceiveSaveRequest(ZRpc rpc, string requestId)
		{
			if (_clientLifecycle.IsActive && !string.IsNullOrWhiteSpace(requestId))
			{
				_pendingRequest = requestId;
				if (!_clientUploadBusy)
				{
					Game.instance.SavePlayerProfile(true, false);
				}
			}
		}

		private void ReceiveSaveAck(ZRpc rpc, string requestId)
		{
			_clientUploadBusy = false;
			CharacterVaultPlugin.SaveStatus?.ShowAccepted(requestId);
			CharacterVaultPlugin.Log.LogInfo("Server accepted character save request " + requestId + ".");
			CharacterVaultPlugin.DisconnectCoordinator?.RecordSaveCommitted(requestId);
			if (_pendingRequest != null)
			{
				Game.instance.SavePlayerProfile(true, false);
			}
		}

		private void ReceiveCommitAck(ZRpc rpc, string requestId)
		{
			CharacterVaultPlugin.SaveStatus?.ShowCommitted(requestId);
			CharacterVaultPlugin.Log.LogInfo("Server confirmed durable character save request " + requestId + ".");
		}

		private void ResetClientState()
		{
			_clientLifecycle.Reset();
			_clientUploadBusy = false;
			_suppressNextClientUpload = false;
			_pendingRequest = null;
			_pendingProfile = null;
			CharacterVaultPlugin.SaveStatus?.Hide();
			CharacterVaultPlugin.DisconnectCoordinator?.RecordConnectionLost();
		}

		private IEnumerator SendUpload(ZRpc rpc, PlayerProfile profile, byte[] data, string requestId)
		{
			string transferId = Guid.NewGuid().ToString("N");
			bool sent = false;
			try
			{
				ZPackage val = ProfileTransferProtocol.Begin(transferId, data.Length, VaultStorage.Hash(data));
				val.Write(requestId);
				val.Write(profile.GetPlayerID());
				rpc.Invoke("CharacterVault_UploadBegin_v1", new object[1] { val });
				for (int offset = 0; offset < data.Length; offset += 65536)
				{
					rpc.Invoke("CharacterVault_UploadChunk_v1", new object[1] { ProfileTransferProtocol.Chunk(transferId, data, offset) });
					yield return null;
				}
				ZPackage val2 = new ZPackage();
				val2.Write(transferId);
				rpc.Invoke("CharacterVault_UploadComplete_v1", new object[1] { val2 });
				sent = true;
			}
			finally
			{
				ProfileTransferService profileTransferService = this;
				if (!sent)
				{
					ZNet instance = ZNet.instance;
					if (((instance != null) ? instance.GetServerRPC() : null) == rpc)
					{
						profileTransferService._clientUploadBusy = false;
						CharacterVaultPlugin.Log.LogWarning("Character save upload " + requestId + " was interrupted before completion.");
					}
				}
			}
		}

		private void ReceiveUploadBegin(ZRpc rpc, ZPackage package)
		{
			if (TryGetVerifiedSession(rpc, out var session))
			{
				IncomingTransfer incomingTransfer = IncomingTransfer.Create(package, 67108864);
				incomingTransfer.RequestId = package.ReadString();
				if (package.ReadLong() != session.CharacterId)
				{
					throw new InvalidDataException("A peer attempted to save a different character.");
				}
				_uploads[rpc] = incomingTransfer;
			}
		}

		private void ReceiveUploadChunk(ZRpc rpc, ZPackage package)
		{
			if (_uploads.TryGetValue(rpc, out var value))
			{
				value.Add(package);
			}
		}

		private void ReceiveUploadComplete(ZRpc rpc, ZPackage package)
		{
			string transferId = package.ReadString();
			if (!_uploads.TryGetValue(rpc, out var value) || !TryGetVerifiedSession(rpc, out var session))
			{
				return;
			}
			_uploads.Remove(rpc);
			byte[] data = value.Complete(transferId);
			if (ValidateProfile(rpc, session, data))
			{
				_finalSaveMonitor.RecordSaveReceived(rpc, value.RequestId);
				if (!SaveAcknowledgementPolicy.CanAcknowledge(session.State))
				{
					CharacterVaultPlugin.Log.LogWarning("Rejected character save " + value.RequestId + " for " + session.Name + ": the player is not permitted to save on this server.");
				}
				else if (session.Enrolling)
				{
					_storage.Commit(session.AccountId, session.Name, data);
					ConfirmCommit(rpc, session, value.RequestId);
				}
				else
				{
					ConfirmReceipt(rpc, session, value.RequestId);
					QueueCommit(rpc, session, value.RequestId, data);
				}
			}
		}

		private void ConfirmReceipt(ZRpc rpc, VaultSession session, string requestId)
		{
			rpc.Invoke("CharacterVault_SaveAck_v1", new object[1] { requestId });
			CharacterVaultPlugin.Log.LogMessage("Accepted character profile for " + session.Name + " for request " + requestId + "; durable commit queued.");
		}

		private void QueueCommit(ZRpc rpc, VaultSession session, string requestId, byte[] data)
		{
			_commits.Enqueue(new PendingCommit(rpc, session, requestId, data));
		}

		private void ConfirmBackgroundCommit(PendingCommit commit)
		{
			CharacterVaultPlugin.Log.LogMessage("Committed character profile for " + commit.Session.Name + " for request " + commit.RequestId + ".");
			if (_sessions.TryGetValue(commit.Rpc, out var value) && value == commit.Session)
			{
				commit.Rpc.Invoke("CharacterVault_CommitAck_v1", new object[1] { commit.RequestId });
				CharacterVaultPlugin.Coordinator?.RecordSaveCommitted(commit.Rpc, commit.RequestId);
				CharacterVaultPlugin.ServerDisconnects?.RecordCommitted(commit.Rpc, commit.RequestId);
			}
		}

		private void ConfirmCommit(ZRpc rpc, VaultSession session, string requestId)
		{
			if (_sessions.TryGetValue(rpc, out var value) && value == session)
			{
				session.Enrolling = false;
				ReleaseEnrollment(rpc);
				rpc.Invoke("CharacterVault_SaveAck_v1", new object[1] { requestId });
				rpc.Invoke("CharacterVault_CommitAck_v1", new object[1] { requestId });
				CharacterVaultPlugin.Log.LogMessage("Saved character profile for " + session.Name + " for request " + requestId + ".");
				CharacterVaultPlugin.Coordinator?.RecordSaveCommitted(rpc, requestId);
				CharacterVaultPlugin.ServerDisconnects?.RecordCommitted(rpc, requestId);
			}
		}

		private bool TryGetVerifiedSession(ZRpc rpc, out VaultSession session)
		{
			session = null;
			if (_sessions.TryGetValue(rpc, out session))
			{
				return session.State.CanSave;
			}
			return false;
		}

		private bool ValidateProfile(ZRpc rpc, VaultSession session, byte[] data)
		{
			try
			{
				ProfileUploadValidator.Validate(session, data);
				return true;
			}
			catch (InvalidDataException ex)
			{
				CharacterVaultPlugin.Log.LogError($"Character profile validation failed for {session.Name}: {ex}");
				_sessions.Remove(rpc);
				_uploads.Remove(rpc);
				ReleaseEnrollment(rpc);
				CharacterVaultRejection.Reject(rpc, "Your character data could not be validated. Please restart the game and try again.", ex.Message);
				return false;
			}
		}

		private bool ReserveEnrollment(ZRpc rpc, VaultSession session)
		{
			if (CharacterVaultPlugin.Settings.AllowMultipleCharacters)
			{
				return true;
			}
			if (_enrollments.TryGetValue(session.AccountId, out var value) && value != rpc)
			{
				return false;
			}
			_enrollments[session.AccountId] = rpc;
			return true;
		}

		private void ReleaseEnrollment(ZRpc rpc)
		{
			string key = _enrollments.FirstOrDefault((KeyValuePair<string, ZRpc> pair) => pair.Value == rpc).Key;
			if (key != null)
			{
				_enrollments.Remove(key);
			}
		}

		private static void Reject(ZRpc rpc, string message)
		{
			CharacterVaultRejection.Reject(rpc, message);
		}

		private static bool IsReady(ZNetPeer peer)
		{
			if (peer?.m_rpc != null && peer.IsReady())
			{
				ISocket socket = peer.m_socket;
				if (socket == null)
				{
					return false;
				}
				return socket.IsConnected();
			}
			return false;
		}

		private static bool CanSave(Dictionary<ZRpc, VaultSession> sessions, ZRpc rpc)
		{
			if (sessions.TryGetValue(rpc, out var value))
			{
				return value.State.CanSave;
			}
			return false;
		}

		private static GameObject FindItem(string name)
		{
			return ((IEnumerable<GameObject>)ObjectDB.instance?.m_items).FirstOrDefault((Func<GameObject, bool>)((GameObject item) => string.Equals(((Object)item).name, name, StringComparison.OrdinalIgnoreCase)));
		}
	}
	internal static class ProfileUploadValidator
	{
		internal static void Validate(VaultSession session, byte[] data)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: 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)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Expected O, but got Unknown
			string text = "character_vault_validation_" + Guid.NewGuid().ToString("N");
			FileSource localSource = SaveApiCompatibility.LocalSource;
			string characterPath = SaveApiCompatibility.GetCharacterPath(localSource, text);
			Directory.CreateDirectory(Path.GetDirectoryName(characterPath));
			File.WriteAllBytes(characterPath, data);
			try
			{
				PlayerProfile val = new PlayerProfile(text, localSource);
				if (!val.Load() || val.GetPlayerID() != session.CharacterId || !string.Equals(val.GetName(), session.Name, StringComparison.Ordinal))
				{
					throw new InvalidDataException("The uploaded profile identity is invalid.");
				}
			}
			finally
			{
				File.Delete(characterPath);
				SaveApiCompatibility.InvalidateCharacterCache();
			}
		}
	}
	internal static class BackupRetention
	{
		private sealed class BackupFile
		{
			internal string Path { get; }

			internal DateTime Timestamp { get; }

			internal BackupFile(string path, DateTime timestamp)
			{
				Path = path;
				Timestamp = timestamp;
			}
		}

		private const int RecentBackupCount = 5;

		private const int DailyBackupCount = 10;

		private const string TimestampFormat = "yyyyMMdd'T'HHmmssfffffff'Z'";

		internal static IReadOnlyList<string> Apply(string directory, string profileName)
		{
			List<BackupFile> source = (from backup in FindBackups(directory, profileName)
				orderby backup.Timestamp descending
				select backup).ToList();
			HashSet<string> retained = new HashSet<string>(from backup in source.Take(5)
				select backup.Path, StringComparer.Ordinal);
			DateTime dailyBoundary = (from backup in source.Take(5)
				select backup.Timestamp.Date).DefaultIfEmpty(DateTime.MinValue).Last();
			IEnumerable<BackupFile> source2 = from @group in (from backup in source.Skip(5)
					where backup.Timestamp.Date < dailyBoundary
					group backup by backup.Timestamp.Date into @group
					orderby @group.Key descending
					select @group).Take(10)
				select @group.OrderBy((BackupFile backup) => backup.Timestamp).First();
			retained.UnionWith(source2.Select((BackupFile backup) => backup.Path));
			List<string> list = new List<string>();
			foreach (BackupFile item in source.Where((BackupFile backup) => !retained.Contains(backup.Path)))
			{
				File.Delete(item.Path);
				list.Add(Path.GetFileName(item.Path));
			}
			return list;
		}

		private static IEnumerable<BackupFile> FindBackups(string directory, string profileName)
		{
			string prefix = profileName + "_";
			string[] files = Directory.GetFiles(directory, "*.fch");
			foreach (string path in files)
			{
				string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
				if (fileNameWithoutExtension.StartsWith(prefix, StringComparison.Ordinal) && DateTime.TryParseExact(fileNameWithoutExtension.Substring(prefix.Length), "yyyyMMdd'T'HHmmssfffffff'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result))
				{
					yield return new BackupFile(path, result);
				}
			}
		}
	}
	internal sealed class VaultStorage : ICharacterProfileCatalog
	{
		private const string BackupDirectory = "backups";

		bool ICharacterProfileCatalog.HasProfile(string accountId)
		{
			return HasProfile(accountId);
		}

		internal bool TryRead(string accountId, string name, out byte[] data)
		{
			string path = ProfilePath(accountId, name);
			if (!File.Exists(path))
			{
				path = FindProfilePath(accountId, name);
			}
			data = (File.Exists(path) ? File.ReadAllBytes(path) : null);
			return data != null;
		}

		private static string FindProfilePath(string accountId, string name)
		{
			string prefix = SafeSegment(accountId) + "_";
			string path = StorageRoot();
			if (!Directory.Exists(path))
			{
				return string.Empty;
			}
			return Directory.GetFiles(path, prefix + "*.fch", SearchOption.TopDirectoryOnly).FirstOrDefault((string path2) => string.Equals(Path.GetFileNameWithoutExtension(path2).Substring(prefix.Length), name, StringComparison.OrdinalIgnoreCase)) ?? string.Empty;
		}

		internal bool HasProfile(string accountId)
		{
			return GetProfileNames(accountId).Count > 0;
		}

		internal IReadOnlyList<string> GetProfileNames(string accountId)
		{
			string prefix = SafeSegment(accountId) + "_";
			string path = StorageRoot();
			if (!Directory.Exists(path))
			{
				return Array.Empty<string>();
			}
			return (from fileName in Directory.GetFiles(path, prefix + "*.fch", SearchOption.TopDirectoryOnly).Select(Path.GetFileNameWithoutExtension)
				where fileName.StartsWith(prefix, StringComparison.Ordinal)
				select fileName.Substring(prefix.Length) into name
				where !string.IsNullOrWhiteSpace(name)
				select name).Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string name) => name, StringComparer.Ordinal).ToArray();
		}

		internal void Commit(string accountId, string name, byte[] data)
		{
			Directory.CreateDirectory(StorageRoot());
			string text = ProfilePath(accountId, name);
			string text2 = text + ".new";
			WriteDurably(text2, data);
			PreserveBackup(data, Path.GetFileNameWithoutExtension(text));
			Replace(text2, text);
		}

		private void PreserveBackup(byte[] data, string profileName)
		{
			string text = Path.Combine(StorageRoot(), "backups");
			Directory.CreateDirectory(text);
			string text2 = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmssfffffff'Z'", CultureInfo.InvariantCulture);
			WriteDurably(Path.Combine(text, profileName + "_" + text2 + ".fch"), data);
			foreach (string item in BackupRetention.Apply(text, profileName))
			{
				CharacterVaultPlugin.Log.LogInfo("Deleted expired character backup " + item + " for profile " + profileName + ".");
			}
		}

		private static string ProfileFileName(string accountId, string name)
		{
			return SafeSegment(accountId) + "_" + SafeSegment(name) + ".fch";
		}

		internal static string SafeSegment(string value)
		{
			return new string(value.Select((char character) => (!char.IsControl(character) && !Enumerable.Contains("<>:\"/\\|?*", character)) ? character : '_').ToArray());
		}

		private string ProfilePath(string accountId, string name)
		{
			return Path.Combine(StorageRoot(), ProfileFileName(accountId, name));
		}

		private static string StorageRoot()
		{
			return Path.Combine(Utils.GetSaveDataPath((FileSource)2), "characters_local");
		}

		private static void WriteDurably(string path, byte[] data)
		{
			using FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough);
			fileStream.Write(data, 0, data.Length);
			fileStream.Flush(flushToDisk: true);
		}

		private static void Replace(string source, string destination)
		{
			if (File.Exists(destination))
			{
				File.Replace(source, destination, null);
			}
			else
			{
				File.Move(source, destination);
			}
		}

		internal static string Hash(byte[] data)
		{
			using SHA256 sHA = SHA256.Create();
			return BitConverter.ToString(sHA.ComputeHash(data)).Replace("-", string.Empty);
		}
	}
	internal sealed class GracefulShutdownCoordinator : IDisposable
	{
		private const string ExitFilePath = "character_vault.drp";

		private const int MaximumConcurrentSaves = 4;

		private const int ShutdownTimeoutSeconds = 90;

		private const int ClientDisconnectGraceSeconds = 2;

		private readonly HashSet<ZNetPeer> _pendingPeers = new HashSet<ZNetPeer>();

		private readonly HashSet<ZNetPeer> _requestedPeers = new HashSet<ZNetPeer>();

		private readonly HashSet<ZRpc> _shutdownPeerRpcs = new HashSet<ZRpc>();

		private readonly Queue<ZNetPeer> _queuedPeers = new Queue<ZNetPeer>();

		private readonly FileSystemWatcher _exitFileWatcher;

		private readonly SynchronizationContext _unityContext;

		private Timer _timeoutTimer;

		private Timer _disconnectTimer;

		private int _disposed;

		private int _exitRequestQueued;

		private volatile bool _watcherFailed;

		private bool _exitRequestPending;

		private bool _shutdownCommitted;

		private string _requestId;

		internal GracefulShutdownCoordinator(SynchronizationContext unityContext)
		{
			_unityContext = unityContext ?? throw new ArgumentNullException("unityContext");
			try
			{
				_exitFileWatcher = CreateExitFileWatcher();
				if (File.Exists("character_vault.drp"))
				{
					QueueExitRequest();
				}
			}
			catch (Exception arg)
			{
				CharacterVaultPlugin.Log.LogError(string.Format("Could not watch {0}; shutdown requests cannot be detected: {1}", "character_vault.drp", arg));
			}
		}

		private void TryStartFromExitFile()
		{
			if (!CanCoordinateShutdown())
			{
				_exitRequestPending = true;
				return;
			}
			_exitRequestPending = false;
			try
			{
				if (File.Exists("character_vault.drp"))
				{
					string text = File.ReadAllText("character_vault.drp").Trim();
					if (!int.TryParse(text, out var result) || result != Process.GetCurrentProcess().Id)
					{
						File.Delete("character_vault.drp");
						CharacterVaultPlugin.Log.LogWarning("Ignored a stale or invalid character_vault.drp request for process '" + text + "'.");
					}
					else
					{
						File.Delete("character_vault.drp");
						Start(ZNet.instance);
					}
				}
			}
			catch (Exception arg)
			{
				CharacterVaultPlugin.Log.LogError(string.Format("Could not process {0}: {1}", "character_vault.drp", arg));
			}
		}

		internal void RecordSaveCommitted(ZRpc peerRpc, string committedRequestId)
		{
			if (_requestId == null || committedRequestId != _requestId)
			{
				return;
			}
			ZNetPeer val = ((IEnumerable<ZNetPeer>)_pendingPeers).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == peerRpc));
			if (val != null && _pendingPeers.Remove(val))
			{
				_requestedPeers.Remove(val);
				CharacterVaultPlugin.Log.LogMessage($"Confirmed graceful character save for {val.m_playerName} ({_pendingPeers.Count} remaining).");
				RequestNextProfiles();
				if (_pendingPeers.Count == 0)
				{
					Complete();
				}
			}
		}

		private static bool CanCoordinateShutdown()
		{
			if (Application.isBatchMode)
			{
				ZNet instance = ZNet.instance;
				if (instance == null)
				{
					return false;
				}
				return instance.IsServer();
			}
			return false;
		}

		private void Start(ZNet network)
		{
			_requestId = Guid.NewGuid().ToString("N");
			_timeoutTimer = new Timer(OnTimeoutElapsed, _requestId, TimeSpan.FromSeconds(90.0), Timeout.InfiniteTimeSpan);
			foreach (ZNetPeer item in network.GetPeers().Where(HasActiveCharacter))
			{
				_pendingPeers.Add(item);
				_shutdownPeerRpcs.Add(item.m_rpc);
				_queuedPeers.Enqueue(item);
			}
			CharacterVaultPlugin.Log.LogMessage($"Shutdown requested; saving {_pendingPeers.Count} connected character(s).");
			RequestNextProfiles();
			CompleteIfFinished();
		}

		internal void ProcessPendingExitRequest()
		{
			if (_exitRequestPending)
			{
				ProcessExitRequest();
			}
		}

		internal bool TryRequestShutdown()
		{
			if (_shutdownCommitted || _requestId != null || !CanCoordinateShutdown())
			{
				return false;
			}
			Start(ZNet.instance);
			return true;
		}

		private void RequestNextProfiles()
		{
			while (_requestedPeers.Count < 4 && _queuedPeers.Count > 0)
			{
				ZNetPeer val = _queuedPeers.Dequeue();
				if (!_pendingPeers.Contains(val) || !HasActiveCharacter(val))
				{
					_pendingPeers.Remove(val);
					continue;
				}
				_requestedPeers.Add(val);
				CharacterVaultPlugin.Transfers.RequestSave(val, _requestId);
			}
		}

		public void Dispose()
		{
			Interlocked.Exchange(ref _disposed, 1);
			_exitFileWatcher?.Dispose();
			_timeoutTimer?.Dispose();
			_disconnectTimer?.Dispose();
		}

		private void ProcessExitRequest()
		{
			Interlocked.Exchange(ref _exitRequestQueued, 0);
			if (Volatile.Read(in _disposed) == 0 && !_shutdownCommitted)
			{
				if (_watcherFailed)
				{
					_watcherFailed = false;
					CharacterVaultPlugin.Log.LogError("The character_vault.drp watcher failed; shutdown requests may no longer be detected.");
				}
				if (_requestId == null)
				{
					TryStartFromExitFile();
				}
			}
		}

		private FileSystemWatcher CreateExitFileWatcher()
		{
			FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(Path.GetFullPath("."), "character_vault.drp");
			fileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
			fileSystemWatcher.Created += OnExitFileChanged;
			fileSystemWatcher.Changed += OnExitFileChanged;
			fileSystemWatcher.Renamed += OnExitFileRenamed;
			fileSystemWatcher.Error += OnWatcherError;
			fileSystemWatcher.EnableRaisingEvents = true;
			return fileSystemWatcher;
		}

		private void OnExitFileChanged(object sender, FileSystemEventArgs args)
		{
			QueueExitRequest();
		}

		private void OnExitFileRenamed(object sender, RenamedEventArgs args)
		{
			QueueExitRequest();
		}

		private void OnWatcherError(object sender, ErrorEventArgs args)
		{
			_watcherFailed = true;
			QueueExitRequest();
		}

		private void QueueExitRequest()
		{
			if (Volatile.Read(in _disposed) == 0 && Interlocked.Exchange(ref _exitRequestQueued, 1) == 0)
			{
				_unityContext.Post(delegate
				{
					ProcessExitRequest();
				}, null);
			}
		}

		private void OnTimeoutElapsed(object state)
		{
			string timedOutRequestId = (string)state;
			if (Volatile.Read(in _disposed) == 0)
			{
				_unityContext.Post(delegate
				{
					CompleteAfterTimeoutIfCurrent(timedOutRequestId);
				}, null);
			}
		}

		private void CompleteAfterTimeoutIfCurrent(string timedOutRequestId)
		{
			if (!_shutdownCommitted && timedOutRequestId == _requestId)
			{
				CompleteAfterTimeout();
			}
		}

		private void CompleteIfFinished()
		{
			if (_pendingPeers.Count == 0 && !_shutdownCommitted)
			{
				Complete();
			}
		}

		private void Complete()
		{
			CharacterVaultPlugin.Log.LogMessage("All connected character profiles were written to disk; requesting normal client disconnection.");
			RequestClientDisconnects();
		}

		private void CompleteAfterTimeout()
		{
			string arg = string.Join(", ", _pendingPeers.Select((ZNetPeer peer) => peer.m_playerName).ToArray());
			CharacterVaultPlugin.Log.LogWarning($"The {90}-second shutdown save timeout expired with " + $"{_pendingPeers.Count} unsaved character(s): {arg}.");
			CharacterVaultPlugin.Log.LogWarning("Requesting normal client disconnection after the character save timeout.");
			RequestClientDisconnects();
		}

		private void RequestClientDisconnects()
		{
			ZNetPeer[] array = ConnectedShutdownPeers();
			ZNetPeer[] array2 = array;
			for (int i = 0; i < array2.Length; i++)
			{
				array2[i].m_rpc.Invoke("Disconnect", Array.Empty<object>());
			}
			if (array.Length == 0)
			{
				ContinueVanillaShutdown();
				return;
			}
			CharacterVaultPlugin.Log.LogMessage($"Requested normal disconnection for {array.Length} client(s); " + $"waiting {2} seconds before the server fallback.");
			_disconnectTimer = new Timer(OnDisconnectGraceElapsed, null, TimeSpan.FromSeconds(2.0), Timeout.InfiniteTimeSpan);
		}

		private ZNetPeer[] ConnectedShutdownPeers()
		{
			ZNet instance = ZNet.instance;
			IEnumerable<ZNetPeer> enumerable = ((instance != null) ? instance.GetPeers() : null);
			return (enumerable ?? Enumerable.Empty<ZNetPeer>()).Where((ZNetPeer peer) => peer?.m_rpc != null && _shutdownPeerRpcs.Contains(peer.m_rpc)).ToArray();
		}

		private void OnDisconnectGraceElapsed(object state)
		{
			if (Volatile.Read(in _disposed) == 0)
			{
				_unityContext.Post(delegate
				{
					CompleteClientDisconnects();
				}, null);
			}
		}

		private void CompleteClientDisconnects()
		{
			ZNetPeer[] array = ConnectedShutdownPeers();
			ZNetPeer[] array2 = array;
			foreach (ZNetPeer val in array2)
			{
				ZNet.instance.Disconnect(val);
			}
			CharacterVaultPlugin.Log.LogMessage((array.Length == 0) ? "All clients disconnected normally; continuing the vanilla shutdown." : $"Closed {array.Length} remaining client connection(s); continuing the vanilla shutdown.");
			ContinueVanillaShutdown();
		}

		private void ContinueVanillaShutdown()
		{
			_timeoutTimer?.Dispose();
			_timeoutTimer = null;
			_disconnectTimer?.Dispose();
			_disconnectTimer = null;
			_pendingPeers.Clear();
			_requestedPeers.Clear();
			_shutdownPeerRpcs.Clear();
			_queuedPeers.Clear();
			_requestId = null;
			_shutdownCommitted = true;
			CharacterVaultPlugin.Log.LogMessage("Starting the vanilla application shutdown.");
			CharacterVaultPlugin.Instance.QuitNextFrame();
		}

		private static bool HasActiveCharacter(ZNetPeer peer)
		{
			if (peer?.m_rpc != null)
			{
				ISocket socket = peer.m_socket;
				if (socket != null && socket.IsConnected())
				{
					return !string.IsNullOrWhiteSpace(peer.m_playerName);
				}
			}
			return false;
		}
	}
	public static class GracefulShutdownApi
	{
		public static bool TryRequest()
		{
			return CharacterVaultPlugin.Coordinator?.TryRequestShutdown() ?? false;
		}
	}
	internal interface IKickSaveRequest
	{
		KickSaveRequestResult Request();
	}
	internal sealed class KickSaveRequestResult
	{
		internal string RequestId { get; }

		internal bool Started { get; }

		internal KickSaveRequestResult(bool started, string requestId = "")
		{
			Started = started;
			RequestId = requestId;
		}
	}
	internal enum KickSaveEligibility
	{
		Unmanaged,
		Rejected,
		SaveRequired
	}
	internal enum KickAction
	{
		Allow,
		AllowWithoutSave,
		WaitForPendingSave,
		RequestSave
	}
	internal static class KickSavePolicy
	{
		internal static KickAction Decide(bool validServerPeer, bool saveAuthorized, bool savePending, KickSaveEligibility eligibility)
		{
			if (!validServerPeer || saveAuthorized)
			{
				return KickAction.Allow;
			}
			if (eligibility == KickSaveEligibility.Rejected)
			{
				return KickAction.AllowWithoutSave;
			}
			if (savePending)
			{
				return KickAction.WaitForPendingSave;
			}
			if (eligibility != KickSaveEligibility.SaveRequired)
			{
				return KickAction.AllowWithoutSave;
			}
			return KickAction.RequestSave;
		}
	}
	internal static class KickSaveRequestExecutor
	{
		internal static KickSaveRequestResult Execute(KickAction action, IKickSaveRequest request)
		{
			if (action != KickAction.RequestSave)
			{
				return new KickSaveRequestResult(started: false);
			}
			return request.Request();
		}
	}
	internal sealed class VoluntaryDisconnectCoordinator : IDisposable
	{
		private const float ConfirmationTimeoutSeconds = 10f;

		private bool _allowApplicationQuit;

		private bool _allowLogout;

		private Game _game;

		private bool _logoutSave;

		private bool _logoutStartScene;

		private string _requestId;

		private VoluntaryExitKind _exitKind;

		private bool _playerEnteredWorld;

		internal bool HasPendingSave => _requestId != null;

		internal bool AllowLogout(Game game, bool save, bool changeToStartScene)
		{
			if (_allowLogout)
			{
				_allowLogout = false;
				CharacterVaultPlugin.Log.LogInfo("Allowing voluntary logout after the final character save was accepted.");
				return true;
			}
			if (!save || !Start(VoluntaryExitKind.Logout, game, save, changeToStartScene))
			{
				return true;
			}
			return false;
		}

		internal void RecordSaveCommitted(string requestId)
		{
			if (!(requestId != _requestId))
			{
				CharacterVaultPlugin.Log.LogMessage("Final voluntary disconnect save " + requestId + " accepted.");
				CharacterVaultPlugin.Transfers.SuppressRedundantDisconnectUpload();
				CompletePendingExit("after the confirmed save");
			}
		}

		internal void RecordConnectionStarted()
		{
			_playerEnteredWorld = false;
			ClearPendingRequest();
		}

		internal void RecordPlayerSpawned()
		{
			_playerEnteredWorld = true;
			CharacterVaultPlugin.Log.LogInfo("CharacterVault final-save protection armed after the local player spawned.");
		}

		internal void RecordConnectionLost()
		{
			if (_requestId != null)
			{
				CharacterVaultPlugin.Log.LogWarning("Connection was lost while final save " + _requestId + " was pending; confirmation is impossible.");
				ClearPendingRequest();
				_playerEnteredWorld = false;
			}
		}

		internal bool AllowMenuQuit()
		{
			if (_allowApplicationQuit)
			{
				return true;
			}
			bool num = Start(VoluntaryExitKind.ApplicationQuit, Game.instance, save: true, startScene: false);
			if (num)
			{
				CharacterVaultPlugin.Log.LogMessage("Intercepted the in-game Quit action; waiting for the final save acceptance.");
			}
			return !num;
		}

		public void Dispose()
		{
			ClearPendingRequest();
		}

		private bool Start(VoluntaryExitKind kind, Game game, bool save, bool startScene)
		{
			string requestId = "disconnect-" + Guid.NewGuid().ToString("N");
			IVoluntaryExitSaveRequest request = new VoluntaryExitSaveRequest(() => CharacterVaultPlugin.Transfers?.BeginFinalDisconnectSave(requestId) ?? false);
			VoluntaryExitSaveAction voluntaryExitSaveAction = VoluntaryExitSavePolicy.Start(_playerEnteredWorld, _requestId != null, request);
			if (voluntaryExitSaveAction != VoluntaryExitSaveAction.WaitForNewSave)
			{
				return voluntaryExitSaveAction == VoluntaryExitSaveAction.WaitForPendingSave;
			}
			_requestId = requestId;
			_exitKind = kind;
			_game = game;
			_logoutSave = save;
			_logoutStartScene = startScene;
			CharacterVaultPlugin.Log.LogMessage("Delayed voluntary " + Describe(kind) + " until final save " + requestId + " is committed.");
			CharacterVaultPlugin.Instance.Run(WaitForConfirmation(requestId));
			return true;
		}

		private IEnumerator WaitForConfirmation(string requestId)
		{
			float deadline = Time.realtimeSinceStartup + 10f;
			while (_requestId == requestId && Time.realtimeSinceStartup < deadline)
			{
				yield return null;
			}
			if (!(_requestId != requestId))
			{
				CharacterVaultPlugin.Log.LogError("Allowing voluntary " + Describe(_exitKind) + " because final save " + requestId + " " + $"was not confirmed within {10f:0} seconds.");
				CompletePendingExit("after the confirmation timeout");
			}
		}

		private void CompletePendingExit(string reason)
		{
			VoluntaryExitKind exitKind = _exitKind;
			Game game = _game;
			bool logoutSave = _logoutSave;
			bool logoutStartScene = _logoutStartScene;
			ClearPendingRequest();
			if (exitKind == VoluntaryExitKind.ApplicationQuit)
			{
				_allowApplicationQuit = true;
				CharacterVaultPlugin.Log.LogInfo("Allowing application quit " + reason + ".");
				Application.Quit();
			}
			else
			{
				_allowLogout = true;
				CharacterVaultPlugin.Log.LogInfo("Allowing logout " + reason + ".");
				game.Logout(logoutSave, logoutStartScene);
			}
		}

		private void ClearPendingRequest()
		{
			_requestId = null;
			_game = null;
		}

		private static string Describe(VoluntaryExitKind kind)
		{
			if (kind != VoluntaryExitKind.ApplicationQuit)
			{
				return "logout";
			}
			return "application quit";
		}
	}
	internal enum VoluntaryExitKind
	{
		Logout,
		ApplicationQuit
	}
	internal sealed class VoluntaryExitSaveRequest : IVoluntaryExitSaveRequest
	{
		private readonly Func<bool> _request;

		internal VoluntaryExitSaveRequest(Func<bool> request)
		{
			_request = request;
		}

		public bool Request()
		{
			return _request();
		}
	}
	internal interface IVoluntaryExitSaveRequest
	{
		bool Request();
	}
	internal enum VoluntaryExitSaveAction
	{
		PassThrough,
		WaitForPendingSave,
		WaitForNewSave
	}
	internal static class VoluntaryExitSavePolicy
	{
		internal static VoluntaryExitSaveAction Start(bool playerEnteredWorld, bool savePending, IVoluntaryExitSaveRequest request)
		{
			if (!playerEnteredWorld)
			{
				return VoluntaryExitSaveAction.PassThrough;
			}
			if (savePending)
			{
				return VoluntaryExitSaveAction.WaitForPendingSave;
			}
			if (!request.Request())
			{
				return VoluntaryExitSaveAction.PassThrough;
			}
			return VoluntaryExitSaveAction.WaitForNewSave;
		}
	}
	internal static class WorldSavePolicy
	{
		internal static void Handle(bool isServer, Action requestCharacterCheckpoint)
		{
			if (isServer)
			{
				requestCharacterCheckpoint();
			}
		}
	}
	internal static class StartingItemGrantPolicy
	{
		internal static bool ApplyEnrollment<TItem>(ClientSaveLifecycle lifecycle, bool isLocalPlayer, IEnumerable<StartingItem> startingItems, Func<string, TItem> findItem, Func<TItem, int, bool> addItem, Action saveProfile, Action<StartingItem> reportFailure) where TItem : class
		{
			if (!lifecycle.RecordSpawn(isLocalPlayer))
			{
				return false;
			}
			foreach (StartingItem startingItem in startingItems)
			{
				if (!Grant(startingItem.Prefab, startingItem.Quantity, findItem, addItem))
				{
					reportFailure(startingItem);
				}
			}
			saveProfile();
			return true;
		}

		internal static bool Grant<TItem>(string prefabName, int quantity, Func<string, TItem> findItem, Func<TItem, int, bool> addItem) where TItem : class
		{
			TItem val = findItem(prefabName);
			if (val != null)
			{
				return addItem(val, quantity);
			}
			return false;
		}
	}
	internal sealed class StartingItem
	{
		internal string Prefab { get; }

		internal int Quantity { get; }

		internal StartingItem(string prefab, int quantity)
		{
			Prefab = prefab;
			Quantity = quantity;
		}
	}
	internal sealed class ServerDisconnectSaveCoordinator : IDisposable
	{
		private const float ConfirmationTimeoutSeconds = 30f;

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

		private readonly HashSet<ZRpc> _authorizedDisconnects = new HashSet<ZRpc>();

		internal bool AllowKick(ZNet network, ZNetPeer peer)
		{
			ZNet obj = network;
			int num;
			int num2;
			if (obj != null && obj.IsServer())
			{
				num = ((peer?.m_rpc != null) ? 1 : 0);
				if (num != 0)
				{
					num2 = (_authorizedDisconnects.Remove(peer.m_rpc) ? 1 : 0);
					goto IL_0063;
				}
			}
			else
			{
				num = 0;
			}
			num2 = 0;
			goto IL_0063;
			IL_0063:
			bool flag = (byte)num2 != 0;
			bool savePending = num != 0 && HasPendingRequest(peer.m_rpc);
			KickSaveEligibility eligibility = ((num != 0) ? (CharacterVaultPlugin.Transfers?.GetKickSaveEligibility(peer) ?? KickSaveEligibility.Unmanaged) : KickSaveEligibility.Unmanaged);
			KickAction action = KickSavePolicy.Decide((byte)num != 0, flag, savePending, eligibility);
			if (TryResolveWithoutSave(action, flag, peer, out var allow))
			{
				return allow;
			}
			string requestId;
			IKickSaveRequest request = new KickSaveRequestOperation(() => new KickSaveRequestResult(TryRequest(peer, "server kick", delegate(string requestId2, bool saved)
			{
				CompleteKick(network, peer, requestId2, saved);
			}, out requestId), requestId));
			KickSaveRequestResult kickSaveRequestResult = KickSaveRequestExecutor.Execute(action, request);
			if (!kickSaveRequestResult.Started)
			{
				CharacterVaultPlugin.Log.LogWarning("A final save could not be requested for " + peer.m_playerName + "; allowing the kick without it.");
				return true;
			}
			CharacterVaultPlugin.Log.LogMessage("Delayed kick for " + peer.m_playerName + " until final save " + kickSaveRequestResult.RequestId + " is committed.");
			return false;
		}

		private static bool TryResolveWithoutSave(KickAction action, bool authorized, ZNetPeer peer, out bool allow)
		{
			allow = action == KickAction.Allow || action == KickAction.AllowWithoutSave;
			if (action == KickAction.Allow && authorized)
			{
				CharacterVaultPlugin.Log.LogInfo("Allowing kick for " + peer.m_playerName + " after its confirmed final save.");
			}
			else
			{
				switch (action)
				{
				case KickAction.AllowWithoutSave:
					CharacterVaultPlugin.Log.LogInfo("Allowing kick for " + peer.m_playerName + " without a character save.");
					break;
				case KickAction.WaitForPendingSave:
					CharacterVaultPlugin.Log.LogWarning("Ignored another kick for " + peer.m_playerName + " while its final save is pending.");
					break;
				}
			}
			return action != KickAction.RequestSave;
		}

		internal bool TryRequest(ZNetPeer peer, string reason, Action<string, bool> completed, out string requestId)
		{
			requestId = null;
			if (peer?.m_rpc != null && completed != null)
			{
				ZNet instance = ZNet.instance;
				if (instance != null && instance.IsServer() && peer.IsReady())
				{
					ProfileTransferService transfers = CharacterVaultPlugin.Transfers;
					if (transfers != null && transfers.CanRequestSave(peer))
					{
						requestId = "server-disconnect-" + Guid.NewGuid().ToString("N");
						_pending[requestId] = new PendingServerSave(peer.m_rpc, peer.m_playerName, reason, completed);
						CharacterVaultPlugin.Log.LogMessage("Requesting final save " + requestId + " for " + peer.m_playerName + " before " + reason + ".");
						CharacterVaultPlugin.Transfers.RequestSave(peer, requestId);
						CharacterVaultPlugin.Instance.Run(WaitForConfirmation(requestId));
						return true;
					}
				}
			}
			return false;
		}

		internal void RecordCommitted(ZRpc rpc, string requestId)
		{
			if (_pending.TryGetValue(requestId, out var value) && value.Rpc == rpc)
			{
				_pending.Remove(requestId);
				_authorizedDisconnects.Add(rpc);
				CharacterVaultPlugin.Log.LogMessage("Final save " + requestId + " for " + value.PlayerName + " committed; authorizing " + value.Reason + ".");
				value.Completed(requestId, arg2: true);
			}
		}

		internal void RecordDisconnected(ZRpc rpc)
		{
			_authorizedDisconnects.Remove(rpc);
			foreach (string item in RequestsFor(rpc))
			{
				CompleteFailed(item, "the connection closed before confirmation");
			}
		}

		internal bool HasPendingSave(ZRpc rpc)
		{
			if (rpc != null)
			{
				return HasPendingRequest(rpc);
			}
			return false;
		}

		public void Dispose()
		{
			_authorizedDisconnects.Clear();
			foreach (string item in new List<string>(_pending.Keys))
			{
				CompleteFailed(item, "CharacterVault unloaded before confirmation");
			}
		}

		private void CompleteKick(ZNet network, ZNetPeer peer, string requestId, bool saved)
		{
			if (!saved)
			{
				CharacterVaultPlugin.Log.LogWarning("Final save " + requestId + " for " + peer.m_playerName + " was not confirmed; proceeding with the kick.");
			}
			else
			{
				CharacterVaultPlugin.Log.LogMessage("Replaying kick for " + peer.m_playerName + " after save " + requestId + ".");
			}
			ZRpc val = peer?.m_rpc;
			if (val == null || peer.m_socket == null)
			{
				return;
			}
			_authorizedDisconnects.Add(val);
			try
			{
				network.Kick(peer.m_socket.GetHostName());
			}
			finally
			{
				_authorizedDisconnects.Remove(val);
			}
		}

		private bool HasPendingRequest(ZRpc rpc)
		{
			foreach (PendingServerSave value in _pending.Values)
			{
				if (value.Rpc == rpc)
				{
					return true;
				}
			}
			return false;
		}

		private IEnumerator WaitForConfirmation(string requestId)
		{
			float deadline = Time.realtimeSinceStartup + 30f;
			while (_pending.ContainsKey(requestId) && Time.realtimeSinceStartup < deadline)
			{
				yield return null;
			}
			if (_pending.ContainsKey(requestId))
			{
				CompleteFailed(requestId, $"no commit acknowledgement arrived within {30f:0} seconds");
			}
		}

		private void CompleteFailed(string requestId, string reason)
		{
			if (_pending.TryGetValue(requestId, out var value))
			{
				_pending.Remove(requestId);
				CharacterVaultPlugin.Log.LogWarning("Final save " + requestId + " for " + value.PlayerName + " was abandoned: " + reason + "; " + value.Reason + " will continue.");
				value.Completed(requestId, arg2: false);
			}
		}

		private List<string> RequestsFor(ZRpc rpc)
		{
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, PendingServerSave> item in _pending)
			{
				if (item.Value.Rpc == rpc)
				{
					list.Add(item.Key);
				}
			}
			return list;
		}
	}
	internal sealed class KickSaveRequestOperation : IKickSaveRequest
	{
		private readonly Func<KickSaveRequestResult> _request;

		internal KickSaveRequestOperation(Func<KickSaveRequestResult> request)
		{
			_request = request;
		}

		public KickSaveRequestResult Request()
		{
			return _request();
		}
	}
	internal sealed class PendingServerSave
	{
		internal Action<string, bool> Completed { get; }

		internal string PlayerName { get; }

		internal string Reason { get; }

		internal ZRpc Rpc { get; }

		internal PendingServerSave(ZRpc rpc, string playerName, string reason, Action<string, bool> completed)
		{
			Rpc = rpc;
			PlayerName = playerName;
			Reason = reason;
			Completed = completed;
		}
	}
	public static class ServerDisconnectApi
	{
		public static bool TrySaveBeforeDisconnect(ZRpc rpc, string reason, Action<bool> completed)
		{
			ZNet instance = ZNet.instance;
			ZNetPeer val = ((instance != null) ? ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate?.m_rpc != null && candidate.m_rpc == rpc)) : null);
			if (val == null || completed == null)
			{
				return false;
			}
			string requestId;
			return CharacterVaultPlugin.ServerDisconnects?.TryRequest(val, reason, delegate(string _, bool saved)
			{
				completed(saved);
			}, out requestId) ?? false;
		}
	}
	[HarmonyPatch(typeof(ZNet), "SaveWorldAndPlayerProfiles")]
	internal static class CharacterVaultManualSavePatch
	{
		private static void Prefix()
		{
			CharacterVaultPlugin.Transfers?.SaveManualClientProfile();
		}
	}
	[HarmonyPatch(typeof(Minimap), "Start")]
	internal static class CharacterVaultSaveStatusPatch
	{
		private static void Postfix(Minimap __instance)
		{
			CharacterVaultPlugin.SaveStatus?.Attach(__instance);
		}
	}
	[HarmonyPatch(typeof(ZNet), "Start")]
	internal static class PendingExitRequestPatch
	{
		private static void Postfix(ZNet __instance)
		{
			if (__instance.IsServer())
			{
				CharacterVaultPlugin.Settings?.InitializeServer();
			}
			CharacterVaultPlugin.Coordinator?.ProcessPendingExitRequest();
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class CharacterVaultConnectionPatch
	{
		private static void Postfix(ZNet __instance, ZNetPeer peer)
		{
			CharacterVaultPlugin.Transfers?.Register(__instance, peer);
		}
	}
	[HarmonyPatch(typeof(ZNet), "SendPeerInfo")]
	internal static class CharacterVaultHelloPatch
	{
		private static void Prefix(ZRpc rpc, bool __runOriginal)
		{
			if (__runOriginal)
			{
				ZNet instance = ZNet.instance;
				if (instance != null && !instance.IsServer())
				{
					CharacterVaultPlugin.Transfers?.SendHello(rpc);
				}
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	internal static class CharacterVaultAdmissionBarrierPatch
	{
		private static bool Prefix(ZRpc rpc, bool __runOriginal)
		{
			if (__runOriginal)
			{
				ZNet instance = ZNet.instance;
				if (instance != null && instance.IsServer())
				{
					return CharacterVaultPlugin.Transfers?.Approve(rpc) ?? false;
				}
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(ZNet), "IsAllowed")]
	internal static class CharacterVaultPermittedListReasonPatch
	{
		private static void Postfix(string hostName, string playerName, SyncedList ___m_bannedList, SyncedList ___m_permittedList, Platform ___m_steamPlatform, bool __result)
		{
			//IL_0019: 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)
			CharacterVaultPlugin.Transfers?.RecordPermission(hostName, __result);
			if (!__result && !IsListed(___m_bannedList, hostName, ___m_steamPlatform) && !___m_bannedList.Contains(playerName) && ___m_permittedList.Count() != 0 && !IsListed(___m_permittedList, hostName, ___m_steamPlatform))
			{
				CharacterVaultRejection.RecordPermittedListRejection(hostName);
			}
		}

		private unsafe static bool IsListed(SyncedList list, string value, Platform steamPlatform)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			PlatformUserID val = default(PlatformUserID);
			if (!PlatformUserID.TryParse(value, ref val))
			{
				((PlatformUserID)(ref val))..ctor(steamPlatform, value);
			}
			if (!list.Contains(((object)(*(PlatformUserID*)(&val))/*cast due to .constrained prefix*/).ToString()))
			{
				if (val.m_platform == steamPlatform)
				{
					return list.Contains(val.m_userID.ToString());
				}
				return false;
			}
			return true;
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	internal static class CharacterVaultPermittedListMessagePatch
	{
		private static void Postfix(ZRpc rpc)
		{
			ZNet instance = ZNet.instance;
			if (instance != null && instance.IsServer())
			{
				CharacterVaultRejection.SendPermittedListRejection(rpc);
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	internal static class CharacterVaultDisconnectPatch
	{
		private static void Prefix(ZNetPeer peer)
		{
			ZNet instance = ZNet.instance;
			bool server = instance != null && instance.IsServer();
			string text = CharacterVaultPlugin.Transfers?.DescribeDisconnect(peer, server) ?? "state unavailable";
			CharacterVaultPlugin.Log?.LogDebug("Observed peer disconnection before CharacterVault cleanup: " + text + ".");
			CharacterVaultPlugin.Transfers?.Remove(peer);
			if (peer?.m_rpc != null)
			{
				CharacterVaultRejection.Remove(peer.m_rpc);
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnDestroy")]
	internal static class CharacterVaultClientNetworkDestroyPatch
	{
		private static void Prefix(ZNet __instance)
		{
			if (!__instance.IsServer())
			{
				ZNetPeer serverPeer = __instance.GetServerPeer();
				string text = CharacterVaultPlugin.Transfers?.DescribeDisconnect(serverPeer, server: false) ?? "state unavailable";
				CharacterVaultPlugin.Log?.LogDebug("Observed client network teardown before CharacterVault cleanup: " + text + ".");
			}
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
	internal static class CharacterVaultPublishPlayFabFailurePatch
	{
		private static void Prefix()
		{
			PlayFabConnectionDiagnostics.PublishBlockingFailure();
		}
	}
	[HarmonyPatch(typeof(ZNet), "InternalKick", new Type[] { typeof(ZNetPeer) })]
	internal static class CharacterVaultKickBarrierPatch
	{
		private static bool Prefix(ZNet __instance, ZNetPeer peer)
		{
			return CharacterVaultPlugin.ServerDisconnects?.AllowKick(__instance, peer) ?? true;
		}
	}
	[HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")]
	internal static class CharacterVaultProfileSavedPatch
	{
		private static void Postfix(PlayerProfile __instance, bool __result)
		{
			if (__result)
			{
				CharacterVaultPlugin.Transfers?.UploadSavedProfile(__instance);
			}
		}
	}
	[HarmonyPatch(typeof(Game), "Logout")]
	internal static class CharacterVaultVoluntaryLogoutPatch
	{
		private static bool Prefix(Game __instance, bool save, bool changeToStartScene)
		{
			CharacterVaultPlugin.Log.LogDebug($"Game.Logout invoked: save={save}, changeToStartScene={changeToStartScene}, " + $"pendingCharacterSave={CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false}.");
			return CharacterVaultPlugin.DisconnectCoordinator?.AllowLogout(__instance, save, changeToStartScene) ?? true;
		}
	}
	[HarmonyPatch(typeof(Game), "ContinueLogout")]
	internal static class CharacterVaultContinueLogoutDiagnosticsPatch
	{
		private static void Prefix(bool save, bool shouldExit, bool changeToStartScene)
		{
			CharacterVaultPlugin.Log.LogDebug($"Game.ContinueLogout invoked: save={save}, shouldExit={shouldExit}, " + $"changeToStartScene={changeToStartScene}, " + $"pendingCharacterSave={CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false}.");
		}
	}
	[HarmonyPatch(typeof(Game), "OnDestroy")]
	internal static class CharacterVaultGameDestroyDiagnosticsPatch
	{
		private static void Prefix()
		{
			CharacterVaultPlugin.Log.LogDebug("Game.OnDestroy invoked: " + $"pendingCharacterSave={CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false}.");
		}
	}
	[HarmonyPatch(typeof(Menu), "OnLogout")]
	internal static class CharacterVaultLogoutButtonDiagnosticsPatch
	{
		private static void Prefix()
		{
			CharacterVaultPlugin.Log?.LogDebug("The in-ga