Decompiled source of InventoryAnticheat v0.1.3

CatosServerInventory.dll

Decompiled 2 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("CatosServerInventory")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.3.0")]
[assembly: AssemblyInformationalVersion("0.1.3+63f19e43acadbcbdacf57fc958061cec03aedd1a")]
[assembly: AssemblyProduct("CatosServerInventory")]
[assembly: AssemblyTitle("CatosServerInventory")]
[assembly: AssemblyVersion("0.1.3.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace CatosServerInventory
{
	internal static class Admission
	{
		private sealed class Pending
		{
			internal ZNetPeer Peer;

			internal DateTime StartedUtc;
		}

		private static readonly Dictionary<long, Pending> PendingPeers = new Dictionary<long, Pending>();

		private static readonly Dictionary<long, DateTime> PendingCheckpointRequests = new Dictionary<long, DateTime>();

		internal static void Validate(ZNetPeer peer, InventorySnapshot incoming)
		{
			PendingPeers.Remove(peer.m_uid);
			if (incoming == null || string.IsNullOrWhiteSpace(incoming.PlayerId))
			{
				Reject(peer, "MissingPlayerIdentity", "The inventory did not identify the character.");
				return;
			}
			string canonicalPlayerId;
			InventorySnapshot inventorySnapshot = Plugin.Store.LoadForPlayer(incoming.PlayerId, incoming.CharacterName, out canonicalPlayerId);
			if (inventorySnapshot == null)
			{
				if (Plugin.AllowNewCharacters.Value && IsAllowedStarter(incoming))
				{
					incoming.ServerId = Plugin.Store.ServerId;
					incoming.Revision = 1L;
					incoming.CapturedUtc = DateTime.UtcNow.ToString("O");
					try
					{
						if (!Plugin.Store.Save(incoming) || Plugin.Store.LoadForPlayer(incoming.PlayerId, incoming.CharacterName, out var _) == null)
						{
							Reject(peer, "SnapshotPersistenceFailed", "The server could not verify the new character snapshot.", "The starter inventory was not enrolled because the shared snapshot could not be verified.");
							return;
						}
					}
					catch (Exception ex)
					{
						Plugin.Log.LogError((object)("Could not persist enrollment for " + incoming.PlayerId + "/" + incoming.CharacterName + ": " + ex.GetType().Name + ": " + ex.Message));
						Reject(peer, "SnapshotPersistenceFailed", "The server could not persist the new character snapshot.", "The starter inventory was not enrolled because the shared snapshot could not be written.");
						return;
					}
					Plugin.Log.LogInfo((object)("Enrolled and verified new character " + incoming.PlayerId + "/" + incoming.CharacterName + "."));
				}
				else
				{
					Reject(peer, "UnenrolledInventory", "No server snapshot exists for this character.", "No server snapshot recorded. Inventory received: " + SnapshotCanonicalizer.DescribeInventory(incoming));
				}
				return;
			}
			if (!string.Equals(incoming.PlayerId, canonicalPlayerId, StringComparison.Ordinal))
			{
				incoming.PlayerId = canonicalPlayerId;
			}
			if (SnapshotCanonicalizer.ContentEquals(inventorySnapshot, incoming))
			{
				Plugin.Log.LogInfo((object)$"Accepted inventory for {incoming.PlayerId}/{incoming.CharacterName}; revision {inventorySnapshot.Revision}.");
				return;
			}
			string text = $"Inventory differs from server snapshot revision {inventorySnapshot.Revision} (stored={inventorySnapshot.ContentHash}, received={incoming.ContentHash}).";
			string text2 = "Items not present in the server snapshot: " + SnapshotCanonicalizer.DescribeUnexpectedItems(inventorySnapshot, incoming);
			if (string.Equals(Plugin.EnforcementMode.Value, "AuditOnly", StringComparison.OrdinalIgnoreCase) || string.Equals(Plugin.RestorePolicy.Value, "AuditOnly", StringComparison.OrdinalIgnoreCase))
			{
				string incidentId = IncidentId();
				RecordIncident(incidentId, incoming.PlayerId, incoming.CharacterName, "SnapshotMismatch", incoming.ContentHash, inventorySnapshot.Revision, text + " " + text2);
				Plugin.Log.LogWarning((object)("AuditOnly would reject " + incoming.PlayerId + "/" + incoming.CharacterName + ": " + text));
				DiscordNotifier.Post(incoming.PlayerId, incoming.CharacterName, "AuditOnlyWouldReject", "SnapshotMismatch", incidentId, text2);
				return;
			}
			if (string.Equals(Plugin.RestorePolicy.Value, "RestoreLastKnownGood", StringComparison.OrdinalIgnoreCase))
			{
				if (InventoryRestorer.TryRestore(SnapshotRuntime.FindPlayer(peer), inventorySnapshot, out var reason))
				{
					string incidentId2 = IncidentId();
					RecordIncident(incidentId2, incoming.PlayerId, incoming.CharacterName, "SnapshotRestored", incoming.ContentHash, inventorySnapshot.Revision, text + " " + text2, "Restored");
					Plugin.Log.LogWarning((object)("Restored authoritative inventory for " + incoming.PlayerId + "/" + incoming.CharacterName + ": " + text));
					DiscordNotifier.Post(incoming.PlayerId, incoming.CharacterName, "Restored", "SnapshotMismatch", incidentId2, text2);
					return;
				}
				text = text + " Restore failed: " + reason;
			}
			Reject(peer, "SnapshotMismatch", text, text2);
		}

		internal static void RequestCheckpoint(ZNetPeer peer)
		{
			if (peer == null || peer.m_rpc == null || PendingPeers.ContainsKey(peer.m_uid) || (PendingCheckpointRequests.TryGetValue(peer.m_uid, out var value) && (DateTime.UtcNow - value).TotalSeconds < (double)Plugin.HandshakeTimeoutSeconds.Value))
			{
				return;
			}
			PendingCheckpointRequests[peer.m_uid] = DateTime.UtcNow;
			try
			{
				peer.m_rpc.Invoke("CatosSI_CheckpointRequest", Array.Empty<object>());
			}
			catch (Exception ex)
			{
				PendingCheckpointRequests.Remove(peer.m_uid);
				Plugin.Log.LogWarning((object)$"Inventory checkpoint request failed for {peer.m_uid}/{peer.m_playerName}: {ex.Message}");
			}
		}

		internal static bool ConsumeCheckpointRequest(long peerUid)
		{
			return PendingCheckpointRequests.Remove(peerUid);
		}

		internal static void Register(ZNetPeer peer)
		{
			PendingPeers[peer.m_uid] = new Pending
			{
				Peer = peer,
				StartedUtc = DateTime.UtcNow
			};
		}

		internal static void TickTimeouts()
		{
			List<long> list = new List<long>();
			foreach (KeyValuePair<long, Pending> pendingPeer in PendingPeers)
			{
				if ((DateTime.UtcNow - pendingPeer.Value.StartedUtc).TotalSeconds > (double)Plugin.HandshakeTimeoutSeconds.Value)
				{
					list.Add(pendingPeer.Key);
				}
			}
			foreach (long item in list)
			{
				if (PendingPeers.TryGetValue(item, out var value))
				{
					Reject(value.Peer, "HandshakeTimeout", "The client did not provide an inventory snapshot.");
				}
				PendingPeers.Remove(item);
			}
			List<long> list2 = new List<long>();
			foreach (KeyValuePair<long, DateTime> pendingCheckpointRequest in PendingCheckpointRequests)
			{
				if ((DateTime.UtcNow - pendingCheckpointRequest.Value).TotalSeconds > (double)Plugin.HandshakeTimeoutSeconds.Value)
				{
					list2.Add(pendingCheckpointRequest.Key);
				}
			}
			foreach (long item2 in list2)
			{
				PendingCheckpointRequests.Remove(item2);
				Plugin.Log.LogWarning((object)$"Inventory checkpoint response timed out for peer {item2}.");
			}
		}

		internal static void Reject(ZNetPeer peer, string reasonCode, string details, string webhookDetails = null)
		{
			if (peer == null)
			{
				return;
			}
			PendingPeers.Remove(peer.m_uid);
			string incidentId = IncidentId();
			RecordIncident(incidentId, peer.m_uid.ToString(), peer.m_playerName, reasonCode, string.Empty, 0L, details + (string.IsNullOrWhiteSpace(webhookDetails) ? string.Empty : (" " + webhookDetails)));
			Plugin.Log.LogWarning((object)$"Rejected peer {peer.m_uid}/{peer.m_playerName}: {reasonCode}: {details}");
			DiscordNotifier.Post(peer.m_uid.ToString(), peer.m_playerName, "Rejected", reasonCode, incidentId, webhookDetails);
			try
			{
				ZRpc rpc = peer.m_rpc;
				if (rpc != null)
				{
					rpc.Invoke("Disconnect", Array.Empty<object>());
				}
			}
			catch
			{
			}
		}

		private static void RecordIncident(string incidentId, string playerId, string characterName, string reasonCode, string observedHash, long storedRevision, string evidence, string status = "Open")
		{
			try
			{
				Plugin.Store.SaveIncident(new IntegrityIncident
				{
					IncidentId = incidentId,
					PlayerId = playerId,
					CharacterName = characterName,
					ReasonCode = reasonCode,
					ObservedHash = observedHash,
					StoredRevision = storedRevision,
					Evidence = evidence,
					Status = status
				});
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Could not persist integrity incident " + incidentId + ": " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		private static bool IsAllowedStarter(InventorySnapshot snapshot)
		{
			if (snapshot.Items.Count == 0)
			{
				return Plugin.AllowEmptyStarterInventory.Value;
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			string[] array = (Plugin.StarterItems.Value ?? string.Empty).Split(new char[1] { ',' });
			foreach (string text in array)
			{
				if (!string.IsNullOrWhiteSpace(text))
				{
					hashSet.Add(text.Trim());
				}
			}
			if (hashSet.Count == 0 || snapshot.Items.Count != hashSet.Count)
			{
				return false;
			}
			HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (SnapshotItem item in snapshot.Items)
			{
				if (item == null || item.Stack != 1 || !hashSet.Contains(item.Name) || !hashSet2.Add(item.Name))
				{
					return false;
				}
			}
			return hashSet2.Count == hashSet.Count;
		}

		private static string IncidentId()
		{
			return Guid.NewGuid().ToString("N").Substring(0, 12);
		}
	}
	internal static class DiscordNotifier
	{
		private static readonly HttpClient Client = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(10.0)
		};

		internal static void Post(string playerId, string playerName, string decision, string reason, string incidentId, string details = null)
		{
			if (!Plugin.DiscordEnabled.Value || string.IsNullOrWhiteSpace(Plugin.DiscordWebhookUrl.Value))
			{
				return;
			}
			string value = "**[CatosServerInventory] " + decision + "**\nPlayer: `" + Safe(playerName) + "`\n" + (Plugin.DiscordIncludePlayerId.Value ? ("Player ID: `" + Safe(playerId) + "`\n") : string.Empty) + "Reason: `" + Safe(reason) + "`\nIncident: `" + Safe(incidentId) + "`" + (string.IsNullOrWhiteSpace(details) ? string.Empty : ("\n" + Safe(details)));
			string json = "{\"content\":\"" + Escape(value) + "\"}";
			Task.Run(async delegate
			{
				try
				{
					StringContent body = new StringContent(json, Encoding.UTF8, "application/json");
					try
					{
						HttpResponseMessage val = await Client.PostAsync(Plugin.DiscordWebhookUrl.Value, (HttpContent)(object)body).ConfigureAwait(continueOnCapturedContext: false);
						try
						{
							if (!val.IsSuccessStatusCode)
							{
								Plugin.Log.LogWarning((object)$"Discord webhook returned {(int)val.StatusCode}.");
							}
						}
						finally
						{
							((IDisposable)val)?.Dispose();
						}
					}
					finally
					{
						((IDisposable)body)?.Dispose();
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("Discord notification failed: " + ex.GetType().Name + ": " + ex.Message));
				}
			});
		}

		private static string Safe(string value)
		{
			if (!string.IsNullOrEmpty(value))
			{
				return value.Replace("`", string.Empty).Replace("@", "@\u200b");
			}
			return "<unknown>";
		}

		private static string Escape(string value)
		{
			return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r")
				.Replace("\n", "\\n");
		}
	}
	internal static class InventoryRestorer
	{
		internal static bool TryRestore(Player player, InventorySnapshot snapshot, out string reason)
		{
			reason = string.Empty;
			if ((Object)(object)player == (Object)null || snapshot == null)
			{
				reason = "Player or snapshot was unavailable.";
				return false;
			}
			Inventory inventory = ((Humanoid)player).GetInventory();
			if (inventory == null)
			{
				reason = "Player inventory was unavailable.";
				return false;
			}
			if ((snapshot.Items ?? new List<SnapshotItem>()).Any((SnapshotItem item) => item == null || item.Stack <= 0 || !string.IsNullOrWhiteSpace(item.CustomDataHash)))
			{
				reason = "The snapshot contains custom item data that cannot be reconstructed from its redacted record.";
				return false;
			}
			List<ItemData> original = (from item in inventory.GetAllItems() ?? new List<ItemData>()
				where item != null
				select item.Clone()).ToList();
			try
			{
				inventory.RemoveAll();
				foreach (SnapshotItem item in snapshot.Items ?? new List<SnapshotItem>())
				{
					ItemData val = CreateItem(item);
					if (val == null || !inventory.AddItem(val))
					{
						throw new InvalidOperationException("Could not add " + item.Name + " to the restored inventory.");
					}
				}
				InventorySnapshot right = SnapshotCanonicalizer.FromPlayer(player, snapshot.ServerId, snapshot.Revision);
				if (!SnapshotCanonicalizer.ContentEquals(snapshot, right))
				{
					throw new InvalidOperationException("The restored inventory did not match the authoritative snapshot.");
				}
				return true;
			}
			catch (Exception ex)
			{
				reason = ex.Message;
				try
				{
					RestoreOriginal(inventory, original);
				}
				catch (Exception ex2)
				{
					reason = reason + " Rollback failed: " + ex2.Message;
				}
				return false;
			}
		}

		private static ItemData CreateItem(SnapshotItem snapshot)
		{
			if (snapshot == null || string.IsNullOrWhiteSpace(snapshot.Name))
			{
				return null;
			}
			ObjectDB instance = ObjectDB.instance;
			GameObject obj = ((instance != null) ? instance.GetItemPrefab(snapshot.Name) : null);
			ItemData val = ((obj != null) ? obj.GetComponent<ItemDrop>() : null)?.m_itemData;
			if (val == null)
			{
				return null;
			}
			ItemData obj2 = val.Clone();
			obj2.m_stack = snapshot.Stack;
			obj2.m_quality = snapshot.Quality;
			obj2.m_variant = snapshot.Variant;
			obj2.m_durability = BitConverter.ToSingle(BitConverter.GetBytes(snapshot.DurabilityBits), 0);
			obj2.m_customData = new Dictionary<string, string>();
			obj2.m_equipped = false;
			return obj2;
		}

		private static void RestoreOriginal(Inventory inventory, IEnumerable<ItemData> original)
		{
			inventory.RemoveAll();
			foreach (ItemData item in original ?? Enumerable.Empty<ItemData>())
			{
				inventory.AddItem(item);
			}
		}
	}
	[DataContract]
	internal sealed class InventorySnapshot
	{
		[DataMember(Order = 1)]
		public int SchemaVersion;

		[DataMember(Order = 2)]
		public string ServerId;

		[DataMember(Order = 3)]
		public string PlayerId;

		[DataMember(Order = 4)]
		public string CharacterName;

		[DataMember(Order = 5)]
		public long Revision;

		[DataMember(Order = 6)]
		public string ContentHash;

		[DataMember(Order = 7)]
		public string CapturedUtc;

		[DataMember(Order = 8)]
		public List<SnapshotItem> Items = new List<SnapshotItem>();

		[DataMember(Order = 9)]
		public string RecordChecksum;

		[DataMember(Order = 10)]
		public string ClusterId;

		[DataMember(Order = 11)]
		public string NodeId;
	}
	[DataContract]
	internal sealed class SnapshotItem
	{
		[DataMember(Order = 1)]
		public string Name;

		[DataMember(Order = 2)]
		public int Stack;

		[DataMember(Order = 3)]
		public int Quality;

		[DataMember(Order = 4)]
		public int Variant;

		[DataMember(Order = 5)]
		public int DurabilityBits;

		[DataMember(Order = 6)]
		public string CustomDataHash;
	}
	[DataContract]
	internal sealed class ServerIdentity
	{
		[DataMember(Order = 1)]
		public int SchemaVersion;

		[DataMember(Order = 2)]
		public string ServerId;

		[DataMember(Order = 3)]
		public string CreatedUtc;

		[DataMember(Order = 4)]
		public string ClusterId;

		[DataMember(Order = 5)]
		public string NodeId;
	}
	[DataContract]
	internal sealed class IntegrityIncident
	{
		[DataMember(Order = 1)]
		public int SchemaVersion;

		[DataMember(Order = 2)]
		public string IncidentId;

		[DataMember(Order = 3)]
		public string PlayerId;

		[DataMember(Order = 4)]
		public string CharacterName;

		[DataMember(Order = 5)]
		public string ReasonCode;

		[DataMember(Order = 6)]
		public string ObservedHash;

		[DataMember(Order = 7)]
		public long StoredRevision;

		[DataMember(Order = 8)]
		public string Evidence;

		[DataMember(Order = 9)]
		public string Status;

		[DataMember(Order = 10)]
		public string CreatedUtc;
	}
	internal static class NetworkPatches
	{
		[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
		internal static class OnNewConnection
		{
			private static void Postfix(ZNetPeer peer)
			{
				//IL_002b: Unknown result type (might be due to invalid IL or missing references)
				//IL_0035: Expected O, but got Unknown
				//IL_0047: Unknown result type (might be due to invalid IL or missing references)
				//IL_0051: Expected O, but got Unknown
				if (!Plugin.Enabled.Value || peer == null || peer.m_rpc == null)
				{
					return;
				}
				try
				{
					peer.m_rpc.Register("CatosSI_Request", new Method(OnRequest));
					peer.m_rpc.Register("CatosSI_CheckpointRequest", new Method(OnCheckpointRequest));
					peer.m_rpc.Register<ZPackage>("CatosSI_Reply", (Action<ZRpc, ZPackage>)OnReply);
					peer.m_rpc.Register<ZPackage>("CatosSI_FinalReply", (Action<ZRpc, ZPackage>)OnFinalReply);
					if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
					{
						ClientFinalCheckpointSent = false;
					}
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("CatosServerInventory RPC registration failed: " + ex.Message));
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		internal static class OnPeerInfo
		{
			private static void Postfix(ZNet __instance, ZRpc rpc)
			{
				if (!Plugin.Enabled.Value || (Object)(object)__instance == (Object)null || !__instance.IsServer())
				{
					return;
				}
				ZNetPeer val = FindPeer(rpc);
				if (val == null)
				{
					return;
				}
				Admission.Register(val);
				try
				{
					rpc.Invoke("CatosSI_Request", Array.Empty<object>());
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)$"Inventory handshake request failed for {val.m_uid}: {ex.Message}");
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "Update")]
		internal static class ServerUpdate
		{
			private static void Postfix(ZNet __instance)
			{
				if (Plugin.Enabled.Value && !((Object)(object)__instance == (Object)null) && __instance.IsServer())
				{
					Admission.TickTimeouts();
					SnapshotRuntime.TickServer();
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "Shutdown")]
		internal static class ServerShutdown
		{
			private static void Prefix(ZNet __instance)
			{
				if ((Object)(object)__instance != (Object)null && __instance.IsServer())
				{
					SnapshotRuntime.CaptureAll("shutdown");
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "Disconnect")]
		internal static class ServerDisconnect
		{
			private static void Prefix(ZNetPeer peer)
			{
				if (!Plugin.Enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || peer == null)
				{
					return;
				}
				Player val = SnapshotRuntime.FindPlayer(peer);
				if ((Object)(object)val == (Object)null)
				{
					Plugin.Log.LogDebug((object)$"Disconnect checkpoint skipped because no live Player was found for peer {peer.m_uid}.");
					return;
				}
				try
				{
					SnapshotRuntime.Capture(val, "disconnect");
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)$"Disconnect checkpoint failed for {peer.m_uid}: {ex.GetType().Name}: {ex.Message}");
				}
			}
		}

		[HarmonyPatch(typeof(ZNet), "SendDisconnect", new Type[] { typeof(ZNetPeer) })]
		internal static class ClientDisconnect
		{
			private static void Prefix(ZNetPeer peer)
			{
				if (!Plugin.Enabled.Value || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || peer == null || peer.m_rpc == null || ClientFinalCheckpointSent)
				{
					return;
				}
				Player localPlayer = Player.m_localPlayer;
				if ((Object)(object)localPlayer == (Object)null)
				{
					return;
				}
				try
				{
					peer.m_rpc.Invoke("CatosSI_FinalReply", new object[1] { BuildSnapshotPackage(localPlayer) });
					ClientFinalCheckpointSent = true;
					Plugin.Log.LogInfo((object)$"Sent final disconnect inventory snapshot for player ID {localPlayer.GetPlayerID()}.");
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("Final disconnect inventory snapshot failed: " + ex.GetType().Name + ": " + ex.Message));
				}
			}
		}

		internal const int Protocol = 1;

		internal const string RequestRpc = "CatosSI_Request";

		internal const string CheckpointRequestRpc = "CatosSI_CheckpointRequest";

		internal const string ReplyRpc = "CatosSI_Reply";

		internal const string FinalReplyRpc = "CatosSI_FinalReply";

		private static ZRpc PendingClientRpc;

		private static bool ClientHandshakePending;

		private static bool ClientCheckpointPending;

		private static bool ClientFinalCheckpointSent;

		private static void OnRequest(ZRpc rpc)
		{
			if (Plugin.Enabled.Value && !((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer())
			{
				PendingClientRpc = rpc;
				ClientHandshakePending = true;
				TrySendClientHandshake();
			}
		}

		private static void OnCheckpointRequest(ZRpc rpc)
		{
			if (Plugin.Enabled.Value && !((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer())
			{
				PendingClientRpc = rpc;
				ClientCheckpointPending = true;
				TrySendClientHandshake();
			}
		}

		internal static void TickClientHandshake()
		{
			if ((ClientHandshakePending || ClientCheckpointPending) && !((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer())
			{
				TrySendClientHandshake();
			}
		}

		private static void TrySendClientHandshake()
		{
			if ((!ClientHandshakePending && !ClientCheckpointPending) || PendingClientRpc == null || (Object)(object)Player.m_localPlayer == (Object)null)
			{
				return;
			}
			try
			{
				Player localPlayer = Player.m_localPlayer;
				if (!((Object)(object)localPlayer == (Object)null))
				{
					PendingClientRpc.Invoke("CatosSI_Reply", new object[1] { BuildSnapshotPackage(localPlayer) });
					PendingClientRpc = null;
					ClientHandshakePending = false;
					ClientCheckpointPending = false;
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("Inventory handshake reply failed: " + ex.Message));
			}
		}

		private static ZPackage BuildSnapshotPackage(Player player)
		{
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Expected O, but got Unknown
			InventorySnapshot inventorySnapshot = SnapshotCanonicalizer.FromPlayer(player, string.Empty, 0L);
			ZPackage val = new ZPackage();
			val.Write(1);
			val.Write(inventorySnapshot.PlayerId ?? string.Empty);
			val.Write(inventorySnapshot.CharacterName ?? string.Empty);
			val.Write(inventorySnapshot.ContentHash ?? string.Empty);
			val.Write(inventorySnapshot.Items.Count);
			foreach (SnapshotItem item in inventorySnapshot.Items)
			{
				val.Write(item.Name ?? string.Empty);
				val.Write(item.Stack);
				val.Write(item.Quality);
				val.Write(item.Variant);
				val.Write(item.DurabilityBits);
				val.Write(item.CustomDataHash ?? string.Empty);
			}
			return val;
		}

		private static void OnReply(ZRpc rpc, ZPackage package)
		{
			if (!Plugin.Enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			ZNetPeer val = FindPeer(rpc);
			if (val == null)
			{
				return;
			}
			try
			{
				InventorySnapshot incoming = ReadSnapshot(package);
				if (Admission.ConsumeCheckpointRequest(val.m_uid))
				{
					SnapshotRuntime.CaptureIncoming(val, incoming, "periodic");
				}
				else
				{
					Admission.Validate(val, incoming);
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"Malformed inventory handshake from {val.m_uid}: {ex.Message}");
				Admission.Reject(val, "MalformedInventorySnapshot", "The inventory handshake was malformed.");
			}
		}

		private static void OnFinalReply(ZRpc rpc, ZPackage package)
		{
			if (!Plugin.Enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			ZNetPeer val = FindPeer(rpc);
			if (val == null)
			{
				return;
			}
			try
			{
				InventorySnapshot inventorySnapshot = ReadSnapshot(package);
				if (string.IsNullOrWhiteSpace(val.m_playerName))
				{
					throw new InvalidDataException("The disconnecting peer has no character name.");
				}
				if (!string.Equals(inventorySnapshot.CharacterName, val.m_playerName, StringComparison.Ordinal))
				{
					Plugin.Log.LogInfo((object)$"Using server peer identity {val.m_playerName} for final disconnect snapshot from {val.m_uid}; client reported {inventorySnapshot.CharacterName}.");
				}
				inventorySnapshot.CharacterName = val.m_playerName;
				if (val.m_playerID != 0L)
				{
					inventorySnapshot.PlayerId = val.m_playerID.ToString();
				}
				SnapshotRuntime.CaptureIncoming(val, inventorySnapshot, "disconnect");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)$"Malformed final disconnect snapshot from {val.m_uid}: {ex.Message}");
			}
		}

		private static InventorySnapshot ReadSnapshot(ZPackage package)
		{
			if (package == null)
			{
				throw new InvalidDataException("Missing package.");
			}
			if (package.ReadInt() != 1)
			{
				throw new InvalidDataException("Unsupported inventory protocol.");
			}
			InventorySnapshot inventorySnapshot = new InventorySnapshot
			{
				SchemaVersion = 1,
				PlayerId = package.ReadString(),
				CharacterName = package.ReadString(),
				ContentHash = package.ReadString(),
				Items = new List<SnapshotItem>()
			};
			int num = package.ReadInt();
			if (num < 0 || num > 1000)
			{
				throw new InvalidDataException("Invalid item count.");
			}
			for (int i = 0; i < num; i++)
			{
				inventorySnapshot.Items.Add(new SnapshotItem
				{
					Name = package.ReadString(),
					Stack = package.ReadInt(),
					Quality = package.ReadInt(),
					Variant = package.ReadInt(),
					DurabilityBits = package.ReadInt(),
					CustomDataHash = package.ReadString()
				});
			}
			if (!string.Equals(SnapshotCanonicalizer.ComputeContentHash(inventorySnapshot.Items), inventorySnapshot.ContentHash, StringComparison.Ordinal))
			{
				throw new InvalidDataException("Inventory content hash mismatch.");
			}
			return inventorySnapshot;
		}

		internal static ZNetPeer FindPeer(ZRpc rpc)
		{
			if (rpc == null || (Object)(object)ZNet.instance == (Object)null)
			{
				return null;
			}
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				if (peer != null && peer.m_rpc == rpc)
				{
					return peer;
				}
			}
			return null;
		}
	}
	[BepInPlugin("com.catosvalheim.serverinventory", "Catos Server Inventory", "0.1.3")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "com.catosvalheim.serverinventory";

		public const string Name = "Catos Server Inventory";

		public const string Version = "0.1.3";

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<string> EnforcementMode;

		internal static ConfigEntry<float> SnapshotIntervalSeconds;

		internal static ConfigEntry<bool> RequireClientHandshake;

		internal static ConfigEntry<float> HandshakeTimeoutSeconds;

		internal static ConfigEntry<bool> AllowNewCharacters;

		internal static ConfigEntry<bool> AllowEmptyStarterInventory;

		internal static ConfigEntry<string> StarterItems;

		internal static ConfigEntry<string> PlayerDataDirectory;

		internal static ConfigEntry<string> DeploymentMode;

		internal static ConfigEntry<string> SharedAuthorityPath;

		internal static ConfigEntry<string> ClusterId;

		internal static ConfigEntry<string> NodeId;

		internal static ConfigEntry<string> RestorePolicy;

		internal static ConfigEntry<float> AuthorityLockTimeoutSeconds;

		internal static ConfigEntry<int> BackupCount;

		internal static ConfigEntry<int> CheckpointQueuePerTick;

		internal static ConfigEntry<bool> DiscordEnabled;

		internal static ConfigEntry<string> DiscordWebhookUrl;

		internal static ConfigEntry<bool> DiscordIncludePlayerId;

		internal static SnapshotStore Store;

		private Harmony _harmony;

		private void Awake()
		{
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Expected O, but got Unknown
			//IL_01be: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Expected O, but got Unknown
			//IL_027b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0285: Expected O, but got Unknown
			//IL_03ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_03d4: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable server inventory snapshots and login validation.");
			EnforcementMode = ((BaseUnityPlugin)this).Config.Bind<string>("General", "EnforcementMode", "AuditOnly", new ConfigDescription("Enforce rejects mismatches; AuditOnly logs and allows them.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "Enforce", "AuditOnly" }), Array.Empty<object>()));
			SnapshotIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SnapshotIntervalSeconds", 15f, "Seconds between scheduled server-side player inventory snapshots.");
			RequireClientHandshake = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "RequireClientHandshake", true, "Require the client component to return an inventory snapshot at login.");
			HandshakeTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "HandshakeTimeoutSeconds", 60f, "Seconds to wait for the client inventory handshake after the peer is ready.");
			AllowNewCharacters = ((BaseUnityPlugin)this).Config.Bind<bool>("StarterPolicy", "AllowNewCharacters", true, "Allow players with no server snapshot to enroll using the starter policy.");
			AllowEmptyStarterInventory = ((BaseUnityPlugin)this).Config.Bind<bool>("StarterPolicy", "AllowEmptyStarterInventory", false, "Allow a new character with an empty inventory to enroll. Disabled by default because Valheim starts new characters with a Rag Tunic and Torch.");
			StarterItems = ((BaseUnityPlugin)this).Config.Bind<string>("StarterPolicy", "StarterItems", "$item_chest_rags,$item_torch", "Comma-separated exact starter item names. Default is Valheim's fresh-character Rag Tunic and Torch.");
			PlayerDataDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("Storage", "PlayerDataDirectory", "", "Server-only snapshot directory. Empty uses BepInEx/config/CatosServerInventory.");
			BackupCount = ((BaseUnityPlugin)this).Config.Bind<int>("Storage", "BackupCount", 3, "Number of previous complete snapshots to retain per player.");
			DeploymentMode = ((BaseUnityPlugin)this).Config.Bind<string>("Deployment", "Mode", "Standalone", new ConfigDescription("Standalone keeps a server-local authority. SharedCluster uses the explicit shared authority path.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "Standalone", "SharedCluster" }), Array.Empty<object>()));
			SharedAuthorityPath = ((BaseUnityPlugin)this).Config.Bind<string>("Deployment", "SharedAuthorityPath", "", "Absolute server-only path shared by the trusted Valheim nodes. Required for SharedCluster.");
			ClusterId = ((BaseUnityPlugin)this).Config.Bind<string>("Deployment", "ClusterId", "", "Persistent logical authority name shared by the trusted nodes.");
			NodeId = ((BaseUnityPlugin)this).Config.Bind<string>("Deployment", "NodeId", "", "Unique node name for this Valheim server, such as heimgard or beta-s01.");
			RestorePolicy = ((BaseUnityPlugin)this).Config.Bind<string>("Deployment", "RestorePolicy", "HoldForReview", new ConfigDescription("On an Enforce mismatch, restore the last good snapshot only when the native adapter can prove an exact result.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { "RestoreLastKnownGood", "HoldForReview", "AuditOnly" }), Array.Empty<object>()));
			AuthorityLockTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Deployment", "AuthorityLockTimeoutSeconds", 5f, "Maximum time to wait for a per-player authority file lock.");
			CheckpointQueuePerTick = ((BaseUnityPlugin)this).Config.Bind<int>("General", "CheckpointQueuePerTick", 1, "Maximum number of queued periodic checkpoints processed per Unity tick.");
			DiscordEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Discord", "Enabled", false, "Enable server-side rejection notifications.");
			DiscordWebhookUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Discord", "WebhookUrl", "", "Server-only Discord webhook URL.");
			DiscordIncludePlayerId = ((BaseUnityPlugin)this).Config.Bind<bool>("Discord", "IncludePlayerId", false, "Include the player ID in Discord notifications.");
			NormalizeConfig();
			string text = (string.Equals(DeploymentMode.Value, "SharedCluster", StringComparison.OrdinalIgnoreCase) ? SharedAuthorityPath.Value : (string.IsNullOrWhiteSpace(PlayerDataDirectory.Value) ? Path.Combine(Paths.ConfigPath, "CatosServerInventory") : PlayerDataDirectory.Value));
			Store = new SnapshotStore(text, BackupCount.Value, DeploymentMode.Value, ClusterId.Value, NodeId.Value, AuthorityLockTimeoutSeconds.Value);
			_harmony = new Harmony("com.catosvalheim.serverinventory");
			_harmony.PatchAll();
			Log.LogInfo((object)string.Format("{0} {1} loaded. Mode={2}; deployment={3}; snapshot interval={4:0.#}s; serverId={5}; storage={6}", "Catos Server Inventory", "0.1.3", EnforcementMode.Value, DeploymentMode.Value, SnapshotIntervalSeconds.Value, Store.ServerId, text));
		}

		private void Update()
		{
			if (Enabled.Value && !((Object)(object)ZNet.instance == (Object)null))
			{
				NetworkPatches.TickClientHandshake();
			}
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchAll("com.catosvalheim.serverinventory");
			}
		}

		private static void NormalizeConfig()
		{
			if (SnapshotIntervalSeconds.Value < 1f || SnapshotIntervalSeconds.Value > 3600f || float.IsNaN(SnapshotIntervalSeconds.Value) || float.IsInfinity(SnapshotIntervalSeconds.Value))
			{
				SnapshotIntervalSeconds.Value = 15f;
			}
			if (HandshakeTimeoutSeconds.Value < 10f || HandshakeTimeoutSeconds.Value > 300f || float.IsNaN(HandshakeTimeoutSeconds.Value) || float.IsInfinity(HandshakeTimeoutSeconds.Value))
			{
				HandshakeTimeoutSeconds.Value = 60f;
			}
			if (BackupCount.Value < 1 || BackupCount.Value > 10)
			{
				BackupCount.Value = 3;
			}
			if (CheckpointQueuePerTick.Value < 1 || CheckpointQueuePerTick.Value > 32)
			{
				CheckpointQueuePerTick.Value = 1;
			}
			if (!string.Equals(DeploymentMode.Value, "Standalone", StringComparison.OrdinalIgnoreCase) && !string.Equals(DeploymentMode.Value, "SharedCluster", StringComparison.OrdinalIgnoreCase))
			{
				DeploymentMode.Value = "Standalone";
			}
			if (AuthorityLockTimeoutSeconds.Value < 0.1f || AuthorityLockTimeoutSeconds.Value > 60f || float.IsNaN(AuthorityLockTimeoutSeconds.Value) || float.IsInfinity(AuthorityLockTimeoutSeconds.Value))
			{
				AuthorityLockTimeoutSeconds.Value = 5f;
			}
			if (string.Equals(DeploymentMode.Value, "SharedCluster", StringComparison.OrdinalIgnoreCase) && (!Path.IsPathRooted(SharedAuthorityPath.Value) || string.IsNullOrWhiteSpace(ClusterId.Value) || string.IsNullOrWhiteSpace(NodeId.Value)))
			{
				Log.LogError((object)"SharedCluster requires an absolute SharedAuthorityPath, ClusterId, and NodeId. Refusing to start shared authority mode.");
				throw new InvalidOperationException("Invalid SharedCluster configuration.");
			}
			if (!string.Equals(EnforcementMode.Value, "Enforce", StringComparison.OrdinalIgnoreCase) && !string.Equals(EnforcementMode.Value, "AuditOnly", StringComparison.OrdinalIgnoreCase))
			{
				EnforcementMode.Value = "AuditOnly";
			}
			if (!string.Equals(RestorePolicy.Value, "RestoreLastKnownGood", StringComparison.OrdinalIgnoreCase) && !string.Equals(RestorePolicy.Value, "HoldForReview", StringComparison.OrdinalIgnoreCase) && !string.Equals(RestorePolicy.Value, "AuditOnly", StringComparison.OrdinalIgnoreCase))
			{
				RestorePolicy.Value = "HoldForReview";
			}
		}
	}
	internal static class SnapshotCanonicalizer
	{
		private sealed class ItemTotal
		{
			internal SnapshotItem Item;

			internal long Stack;
		}

		internal static InventorySnapshot FromPlayer(Player player, string serverId, long revision)
		{
			if ((Object)(object)player == (Object)null)
			{
				throw new ArgumentNullException("player");
			}
			Inventory obj = ((Humanoid)player).GetInventory() ?? throw new InvalidOperationException("Player inventory is unavailable.");
			InventorySnapshot inventorySnapshot = new InventorySnapshot
			{
				SchemaVersion = 1,
				ServerId = serverId,
				PlayerId = player.GetPlayerID().ToString(),
				CharacterName = (player.GetPlayerName() ?? string.Empty),
				Revision = revision,
				CapturedUtc = DateTime.UtcNow.ToString("O")
			};
			foreach (ItemData item in obj.GetAllItems() ?? new List<ItemData>())
			{
				if (item != null && item.m_shared != null)
				{
					inventorySnapshot.Items.Add(new SnapshotItem
					{
						Name = (item.m_shared.m_name ?? string.Empty),
						Stack = item.m_stack,
						Quality = item.m_quality,
						Variant = item.m_variant,
						DurabilityBits = BitConverter.ToInt32(BitConverter.GetBytes(item.m_durability), 0),
						CustomDataHash = HashCustomData(item.m_customData)
					});
				}
			}
			inventorySnapshot.Items = inventorySnapshot.Items.OrderBy<SnapshotItem, string>((SnapshotItem i) => i.Name, StringComparer.Ordinal).ThenBy((SnapshotItem i) => i.Quality).ThenBy((SnapshotItem i) => i.Variant)
				.ThenBy((SnapshotItem i) => i.DurabilityBits)
				.ThenBy<SnapshotItem, string>((SnapshotItem i) => i.CustomDataHash, StringComparer.Ordinal)
				.ThenBy((SnapshotItem i) => i.Stack)
				.ToList();
			inventorySnapshot.ContentHash = ComputeContentHash(inventorySnapshot.Items);
			return inventorySnapshot;
		}

		internal static string ComputeContentHash(IEnumerable<SnapshotItem> items)
		{
			StringBuilder stringBuilder = new StringBuilder();
			foreach (SnapshotItem item in items ?? Enumerable.Empty<SnapshotItem>())
			{
				stringBuilder.Append(item.Name ?? string.Empty).Append('\u001f').Append(item.Stack)
					.Append('\u001f')
					.Append(item.Quality)
					.Append('\u001f')
					.Append(item.Variant)
					.Append('\u001f')
					.Append(item.DurabilityBits)
					.Append('\u001f')
					.Append(item.CustomDataHash ?? string.Empty)
					.Append('\u001e');
			}
			return Sha256(stringBuilder.ToString());
		}

		internal static string ComputeRecordChecksum(InventorySnapshot snapshot)
		{
			if (snapshot == null)
			{
				return string.Empty;
			}
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append(snapshot.SchemaVersion).Append('\u001f').Append(snapshot.ServerId ?? string.Empty)
				.Append('\u001f')
				.Append(snapshot.ClusterId ?? string.Empty)
				.Append('\u001f')
				.Append(snapshot.NodeId ?? string.Empty)
				.Append('\u001f')
				.Append(snapshot.PlayerId ?? string.Empty)
				.Append('\u001f')
				.Append(snapshot.CharacterName ?? string.Empty)
				.Append('\u001f')
				.Append(snapshot.Revision)
				.Append('\u001f')
				.Append(snapshot.ContentHash ?? string.Empty)
				.Append('\u001f')
				.Append(snapshot.CapturedUtc ?? string.Empty)
				.Append('\u001e')
				.Append(ComputeContentHash(snapshot.Items ?? new List<SnapshotItem>()));
			return Sha256(stringBuilder.ToString());
		}

		internal static bool IsContentValid(InventorySnapshot snapshot)
		{
			if (snapshot != null)
			{
				return string.Equals(ComputeContentHash(snapshot.Items ?? new List<SnapshotItem>()), snapshot.ContentHash, StringComparison.Ordinal);
			}
			return false;
		}

		internal static bool ContentEquals(InventorySnapshot left, InventorySnapshot right)
		{
			if (left != null && right != null)
			{
				return string.Equals(left.ContentHash, right.ContentHash, StringComparison.Ordinal);
			}
			return false;
		}

		internal static string DescribeInventory(InventorySnapshot snapshot)
		{
			return DescribeItems(snapshot?.Items);
		}

		internal static string DescribeUnexpectedItems(InventorySnapshot stored, InventorySnapshot incoming)
		{
			Dictionary<string, ItemTotal> dictionary = Totals(stored?.Items);
			List<SnapshotItem> list = new List<SnapshotItem>();
			foreach (KeyValuePair<string, ItemTotal> item in Totals(incoming?.Items))
			{
				ItemTotal value;
				long num = (dictionary.TryGetValue(item.Key, out value) ? value.Stack : 0);
				long num2 = item.Value.Stack - num;
				if (num2 > 0)
				{
					SnapshotItem snapshotItem = Copy(item.Value.Item);
					snapshotItem.Stack = (int)((num2 > int.MaxValue) ? int.MaxValue : num2);
					list.Add(snapshotItem);
				}
			}
			if (list.Count != 0)
			{
				return DescribeItems(list);
			}
			return "No additional item quantities identified; the mismatch involves missing or changed snapshot data.";
		}

		private static Dictionary<string, ItemTotal> Totals(IEnumerable<SnapshotItem> items)
		{
			Dictionary<string, ItemTotal> dictionary = new Dictionary<string, ItemTotal>(StringComparer.Ordinal);
			foreach (SnapshotItem item in items ?? Enumerable.Empty<SnapshotItem>())
			{
				if (item != null)
				{
					string key = IdentityKey(item);
					if (!dictionary.TryGetValue(key, out var value))
					{
						dictionary[key] = new ItemTotal
						{
							Item = item,
							Stack = item.Stack
						};
					}
					else
					{
						value.Stack += item.Stack;
					}
				}
			}
			return dictionary;
		}

		private static string DescribeItems(IEnumerable<SnapshotItem> items)
		{
			List<ItemTotal> list = Totals(items).Values.OrderBy<ItemTotal, string>((ItemTotal i) => i.Item.Name, StringComparer.Ordinal).ToList();
			if (list.Count == 0)
			{
				return "(empty inventory)";
			}
			List<string> list2 = new List<string>();
			foreach (ItemTotal item in list.Take(40))
			{
				string text = HumanizeName(item.Item.Name);
				if (item.Item.Quality > 1)
				{
					text += $" (quality {item.Item.Quality})";
				}
				if (item.Item.Variant != 0)
				{
					text += $" (variant {item.Item.Variant})";
				}
				list2.Add($"{item.Stack}x {text}");
			}
			if (list.Count > 40)
			{
				list2.Add($"+{list.Count - 40} more item types");
			}
			string text2 = string.Join(", ", list2);
			if (text2.Length > 1800)
			{
				return text2.Substring(0, 1797) + "...";
			}
			return text2;
		}

		private static string IdentityKey(SnapshotItem item)
		{
			return (item.Name ?? string.Empty) + "\u001f" + item.Quality + "\u001f" + item.Variant + "\u001f" + item.DurabilityBits + "\u001f" + (item.CustomDataHash ?? string.Empty);
		}

		private static SnapshotItem Copy(SnapshotItem item)
		{
			return new SnapshotItem
			{
				Name = item.Name,
				Stack = item.Stack,
				Quality = item.Quality,
				Variant = item.Variant,
				DurabilityBits = item.DurabilityBits,
				CustomDataHash = item.CustomDataHash
			};
		}

		private static string HumanizeName(string value)
		{
			string text = (string.IsNullOrWhiteSpace(value) ? "Unknown item" : value.Trim());
			string[] array = new string[3] { "$item_", "$piece_", "$enemy_" };
			foreach (string text2 in array)
			{
				if (text.StartsWith(text2, StringComparison.OrdinalIgnoreCase))
				{
					text = text.Substring(text2.Length);
				}
			}
			text = text.Replace('_', ' ').Replace('-', ' ');
			StringBuilder stringBuilder = new StringBuilder();
			for (int j = 0; j < text.Length; j++)
			{
				if (j > 0 && char.IsUpper(text[j]) && char.IsLower(text[j - 1]))
				{
					stringBuilder.Append(' ');
				}
				stringBuilder.Append(text[j]);
			}
			string text3 = stringBuilder.ToString().Trim();
			if (text3.Length != 0)
			{
				return char.ToUpperInvariant(text3[0]) + text3.Substring(1);
			}
			return "Unknown item";
		}

		private static string HashCustomData(IDictionary<string, string> data)
		{
			if (data == null || data.Count == 0)
			{
				return string.Empty;
			}
			StringBuilder stringBuilder = new StringBuilder();
			foreach (KeyValuePair<string, string> item in data.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> p) => p.Key, StringComparer.Ordinal))
			{
				stringBuilder.Append(item.Key ?? string.Empty).Append('=').Append(item.Value ?? string.Empty)
					.Append('\u001e');
			}
			return Sha256(stringBuilder.ToString());
		}

		private static string Sha256(string value)
		{
			using SHA256 sHA = SHA256.Create();
			return BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(value ?? string.Empty))).Replace("-", string.Empty);
		}
	}
	internal static class SnapshotRuntime
	{
		private sealed class CheckpointRequest
		{
			internal Player Player;

			internal string Source;
		}

		private static readonly Dictionary<string, long> Revisions = new Dictionary<string, long>(StringComparer.Ordinal);

		private static readonly Queue<CheckpointRequest> Queue = new Queue<CheckpointRequest>();

		private static readonly HashSet<string> QueuedPlayers = new HashSet<string>(StringComparer.Ordinal);

		private static float _nextSnapshot;

		internal static void TickServer()
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				if (Time.time >= _nextSnapshot)
				{
					_nextSnapshot = Time.time + Plugin.SnapshotIntervalSeconds.Value;
					EnqueueAll("periodic");
					RequestClientCheckpoints();
				}
				ProcessQueue(Plugin.CheckpointQueuePerTick.Value);
			}
		}

		internal static void EnqueueAll(string source)
		{
			int num = 0;
			foreach (Player item in Player.GetAllPlayers() ?? new List<Player>())
			{
				if (!((Object)(object)item == (Object)null))
				{
					Enqueue(item, source);
					num++;
				}
			}
			if (num == 0)
			{
				ZNet instance = ZNet.instance;
				if (((instance == null) ? ((int?)null) : instance.GetPeers()?.Count).GetValueOrDefault() > 0)
				{
					Plugin.Log.LogDebug((object)(source + " checkpoint found no server-side Player objects; requesting client snapshots."));
				}
			}
		}

		internal static void RequestClientCheckpoints()
		{
			ZNet instance = ZNet.instance;
			foreach (ZNetPeer item in ((instance != null) ? instance.GetPeers() : null) ?? new List<ZNetPeer>())
			{
				Admission.RequestCheckpoint(item);
			}
		}

		internal static void CaptureIncoming(ZNetPeer peer, InventorySnapshot incoming, string source)
		{
			if (peer == null || incoming == null || string.IsNullOrWhiteSpace(incoming.PlayerId))
			{
				return;
			}
			if (!string.Equals(peer.m_playerName ?? string.Empty, incoming.CharacterName ?? string.Empty, StringComparison.Ordinal))
			{
				Plugin.Log.LogWarning((object)$"Ignored {source} snapshot for peer {peer.m_uid}/{peer.m_playerName}: character name did not match ({incoming.CharacterName}).");
				return;
			}
			string canonicalPlayerId;
			InventorySnapshot inventorySnapshot = Plugin.Store.LoadForPlayer(incoming.PlayerId, incoming.CharacterName, out canonicalPlayerId);
			if (inventorySnapshot == null)
			{
				Plugin.Log.LogWarning((object)("Ignored " + source + " snapshot for " + incoming.PlayerId + "/" + incoming.CharacterName + ": no enrolled server snapshot exists."));
				return;
			}
			incoming.PlayerId = canonicalPlayerId;
			incoming.Revision = inventorySnapshot.Revision + 1;
			incoming.ServerId = Plugin.Store.ServerId;
			incoming.CapturedUtc = DateTime.UtcNow.ToString("O");
			try
			{
				if (Plugin.Store.Save(incoming))
				{
					Revisions[incoming.PlayerId] = incoming.Revision;
					string text = $"Saved {source} client snapshot for {incoming.PlayerId}/{incoming.CharacterName} revision {incoming.Revision} hash {incoming.ContentHash}.";
					if (string.Equals(source, "disconnect", StringComparison.Ordinal))
					{
						Plugin.Log.LogInfo((object)text);
					}
					else
					{
						Plugin.Log.LogDebug((object)text);
					}
				}
				else
				{
					Plugin.Log.LogWarning((object)("Could not save " + source + " client snapshot for " + incoming.PlayerId + "/" + incoming.CharacterName + "; the previous snapshot was retained."));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("Snapshot failed for " + incoming.PlayerId + "/" + incoming.CharacterName + ": " + ex.GetType().Name + ": " + ex.Message));
			}
		}

		internal static void Enqueue(Player player, string source)
		{
			if (!((Object)(object)player == (Object)null))
			{
				string item = player.GetPlayerID().ToString();
				if (QueuedPlayers.Add(item))
				{
					Queue.Enqueue(new CheckpointRequest
					{
						Player = player,
						Source = source
					});
				}
			}
		}

		internal static void ProcessQueue(int budget)
		{
			int num = 0;
			while (num++ < Math.Max(1, budget) && Queue.Count > 0)
			{
				CheckpointRequest checkpointRequest = Queue.Dequeue();
				string text = (((Object)(object)checkpointRequest.Player == (Object)null) ? string.Empty : checkpointRequest.Player.GetPlayerID().ToString());
				if (!string.IsNullOrWhiteSpace(text))
				{
					QueuedPlayers.Remove(text);
				}
				if (!((Object)(object)checkpointRequest.Player == (Object)null))
				{
					try
					{
						Capture(checkpointRequest.Player, checkpointRequest.Source);
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("Snapshot failed for " + SafePlayer(checkpointRequest.Player) + ": " + ex.GetType().Name + ": " + ex.Message));
					}
				}
			}
		}

		internal static void CaptureAll(string source)
		{
			foreach (Player item in Player.GetAllPlayers() ?? new List<Player>())
			{
				if (!((Object)(object)item == (Object)null))
				{
					try
					{
						Capture(item, source);
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("Snapshot failed for " + SafePlayer(item) + ": " + ex.GetType().Name + ": " + ex.Message));
					}
				}
			}
		}

		internal static void Capture(Player player, string source)
		{
			string text = player.GetPlayerID().ToString();
			string canonicalPlayerId;
			InventorySnapshot inventorySnapshot = Plugin.Store.LoadForPlayer(text, player.GetPlayerName(), out canonicalPlayerId);
			long value;
			long num = (Revisions.TryGetValue(text, out value) ? value : (inventorySnapshot?.Revision ?? 0));
			InventorySnapshot inventorySnapshot2 = SnapshotCanonicalizer.FromPlayer(player, Plugin.Store.ServerId, num + 1);
			if (Plugin.Store.Save(inventorySnapshot2))
			{
				Revisions[text] = inventorySnapshot2.Revision;
				Plugin.Log.LogDebug((object)$"Saved {source} snapshot for {SafePlayer(player)} revision {inventorySnapshot2.Revision} hash {inventorySnapshot2.ContentHash}.");
			}
			else
			{
				Plugin.Log.LogWarning((object)$"Could not save {source} snapshot for {SafePlayer(player)} revision {inventorySnapshot2.Revision}; the previous snapshot was retained.");
			}
		}

		internal static Player FindPlayer(ZNetPeer peer)
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			if (peer == null)
			{
				return null;
			}
			foreach (Player item in Player.GetAllPlayers() ?? new List<Player>())
			{
				if (!((Object)(object)item == (Object)null))
				{
					if (peer.m_playerID != 0L && item.GetPlayerID() == peer.m_playerID)
					{
						return item;
					}
					if (((Character)item).GetOwner() == peer.m_uid)
					{
						return item;
					}
					ZDOID zDOID = ((Character)item).GetZDOID();
					if (((ZDOID)(ref peer.m_characterID)).UserID != 0L && ((ZDOID)(ref zDOID)).UserID == ((ZDOID)(ref peer.m_characterID)).UserID && ((ZDOID)(ref zDOID)).ID == ((ZDOID)(ref peer.m_characterID)).ID)
					{
						return item;
					}
				}
			}
			return null;
		}

		private static string SafePlayer(Player player)
		{
			if (!((Object)(object)player == (Object)null))
			{
				return $"{player.GetPlayerID()}/{player.GetPlayerName()}";
			}
			return "<null>";
		}
	}
	internal sealed class SnapshotStore
	{
		private readonly string _players;

		private readonly string _incidents;

		private readonly string _identityPath;

		private readonly int _backupCount;

		private readonly bool _shared;

		private readonly string _clusterId;

		private readonly string _nodeId;

		private readonly double _lockTimeoutSeconds;

		private ServerIdentity _identity;

		internal string ServerId => _identity.ServerId;

		internal string ClusterId
		{
			get
			{
				if (!_shared)
				{
					return string.Empty;
				}
				return _clusterId;
			}
		}

		internal string NodeId
		{
			get
			{
				if (!_shared)
				{
					return string.Empty;
				}
				return _nodeId;
			}
		}

		internal SnapshotStore(string root, int backupCount, string mode, string clusterId, string nodeId, float lockTimeoutSeconds)
		{
			_shared = string.Equals(mode, "SharedCluster", StringComparison.OrdinalIgnoreCase);
			_clusterId = clusterId ?? string.Empty;
			_nodeId = nodeId ?? string.Empty;
			_backupCount = Math.Max(1, Math.Min(10, backupCount));
			_lockTimeoutSeconds = Math.Max(0.1, Math.Min(60.0, lockTimeoutSeconds));
			if (string.IsNullOrWhiteSpace(root))
			{
				throw new InvalidOperationException("Snapshot storage path cannot be empty.");
			}
			if (_shared && !Path.IsPathRooted(root))
			{
				throw new InvalidOperationException("SharedCluster storage path must be absolute.");
			}
			if (!_shared && !Path.IsPathRooted(root))
			{
				root = Path.GetFullPath(root);
			}
			_players = Path.Combine(root, "players");
			_incidents = Path.Combine(root, "incidents");
			_identityPath = (_shared ? Path.Combine(root, "nodes", SafePathPart(_nodeId), "node-identity.json") : Path.Combine(root, "server-identity.json"));
			Directory.CreateDirectory(_players);
			Directory.CreateDirectory(_incidents);
			Directory.CreateDirectory(Path.GetDirectoryName(_identityPath));
			VerifyWritable(_players);
			VerifyWritable(_incidents);
			VerifyWritable(Path.GetDirectoryName(_identityPath));
			_identity = LoadOrCreateIdentity();
		}

		internal InventorySnapshot Load(string playerId)
		{
			using (AcquirePlayerLock(playerId))
			{
				return LoadUnsafe(playerId);
			}
		}

		internal InventorySnapshot LoadForPlayer(string playerId, string characterName, out string canonicalPlayerId)
		{
			canonicalPlayerId = playerId;
			InventorySnapshot inventorySnapshot = Load(playerId);
			if (inventorySnapshot != null || !_shared || string.IsNullOrWhiteSpace(characterName))
			{
				return inventorySnapshot;
			}
			InventorySnapshot inventorySnapshot2 = FindUniqueByCharacterName(characterName);
			if (inventorySnapshot2 == null)
			{
				return null;
			}
			canonicalPlayerId = inventorySnapshot2.PlayerId;
			Plugin.Log.LogWarning((object)("Resolved shared snapshot for " + characterName + " through the stored character name because the local player ID changed from " + playerId + " to " + canonicalPlayerId + "."));
			return inventorySnapshot2;
		}

		internal bool Save(InventorySnapshot snapshot)
		{
			if (snapshot == null || string.IsNullOrWhiteSpace(snapshot.PlayerId))
			{
				return false;
			}
			if (_shared && !string.IsNullOrWhiteSpace(snapshot.CharacterName))
			{
				InventorySnapshot inventorySnapshot = FindUniqueByCharacterName(snapshot.CharacterName);
				if (inventorySnapshot != null && !string.Equals(inventorySnapshot.PlayerId, snapshot.PlayerId, StringComparison.Ordinal))
				{
					snapshot.PlayerId = inventorySnapshot.PlayerId;
				}
			}
			using (AcquirePlayerLock(snapshot.PlayerId))
			{
				InventorySnapshot inventorySnapshot2 = LoadUnsafe(snapshot.PlayerId);
				if (inventorySnapshot2 != null && snapshot.Revision <= inventorySnapshot2.Revision)
				{
					Plugin.Log.LogWarning((object)$"Suppressed stale snapshot revision {snapshot.Revision} for {snapshot.PlayerId}; current revision is {inventorySnapshot2.Revision}.");
					return false;
				}
				snapshot.ServerId = ServerId;
				snapshot.SchemaVersion = 1;
				snapshot.ClusterId = ClusterId;
				snapshot.NodeId = NodeId;
				snapshot.Items = snapshot.Items ?? new List<SnapshotItem>();
				snapshot.ContentHash = SnapshotCanonicalizer.ComputeContentHash(snapshot.Items);
				snapshot.RecordChecksum = SnapshotCanonicalizer.ComputeRecordChecksum(snapshot);
				string playerPath = GetPlayerPath(snapshot.PlayerId);
				string text = playerPath + ".tmp";
				WriteDurable(text, snapshot);
				try
				{
					RotateBackups(snapshot.PlayerId);
					if (File.Exists(playerPath))
					{
						try
						{
							File.Replace(text, playerPath, GetBackupPath(snapshot.PlayerId, 1), ignoreMetadataErrors: true);
						}
						catch (PlatformNotSupportedException)
						{
							ReplaceWithMove(text, playerPath, snapshot.PlayerId);
						}
						catch (IOException)
						{
							ReplaceWithMove(text, playerPath, snapshot.PlayerId);
						}
					}
					else
					{
						File.Move(text, playerPath);
					}
					InventorySnapshot inventorySnapshot3 = LoadUnsafe(snapshot.PlayerId);
					if (inventorySnapshot3 == null || inventorySnapshot3.Revision != snapshot.Revision || !string.Equals(inventorySnapshot3.ContentHash, snapshot.ContentHash, StringComparison.Ordinal))
					{
						throw new IOException("Snapshot verification failed after writing " + Path.GetFileName(playerPath) + ".");
					}
					return true;
				}
				catch
				{
					TryDelete(text);
					throw;
				}
			}
		}

		internal bool HasSnapshot(string playerId)
		{
			return Load(playerId) != null;
		}

		internal void SaveIncident(IntegrityIncident incident)
		{
			if (incident == null || string.IsNullOrWhiteSpace(incident.IncidentId))
			{
				return;
			}
			incident.SchemaVersion = 1;
			incident.IncidentId = Limit(incident.IncidentId, 64);
			incident.PlayerId = Limit(incident.PlayerId, 128);
			incident.CharacterName = Limit(incident.CharacterName, 128);
			incident.ReasonCode = Limit(incident.ReasonCode, 64);
			incident.ObservedHash = Limit(incident.ObservedHash, 128);
			incident.Evidence = Limit(incident.Evidence, 4000);
			incident.Status = Limit(string.IsNullOrWhiteSpace(incident.Status) ? "Open" : incident.Status, 32);
			incident.CreatedUtc = (string.IsNullOrWhiteSpace(incident.CreatedUtc) ? DateTime.UtcNow.ToString("O") : Limit(incident.CreatedUtc, 64));
			string text = Path.Combine(_incidents, SafePathPart(incident.IncidentId) + ".json");
			string text2 = text + ".tmp";
			using (AcquireLock(text + ".lock"))
			{
				WriteDurable(text2, incident);
				try
				{
					if (File.Exists(text))
					{
						try
						{
							File.Replace(text2, text, null, ignoreMetadataErrors: true);
							return;
						}
						catch (PlatformNotSupportedException)
						{
							File.Delete(text);
							File.Move(text2, text);
							return;
						}
						catch (IOException)
						{
							File.Delete(text);
							File.Move(text2, text);
							return;
						}
					}
					File.Move(text2, text);
				}
				catch
				{
					TryDelete(text2);
					throw;
				}
			}
		}

		private InventorySnapshot LoadUnsafe(string playerId)
		{
			foreach (string candidatePath in GetCandidatePaths(playerId))
			{
				if (!File.Exists(candidatePath))
				{
					continue;
				}
				try
				{
					InventorySnapshot inventorySnapshot = Read<InventorySnapshot>(candidatePath);
					if (!IsUsable(inventorySnapshot, playerId))
					{
						Plugin.Log.LogWarning((object)("Ignoring invalid snapshot record " + Path.GetFileName(candidatePath) + " for " + playerId + "."));
						continue;
					}
					if (string.IsNullOrWhiteSpace(inventorySnapshot.RecordChecksum))
					{
						Plugin.Log.LogWarning((object)("Using legacy snapshot without a record checksum for " + playerId + "; it will be upgraded on the next checkpoint."));
					}
					return inventorySnapshot;
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("Could not read snapshot record " + Path.GetFileName(candidatePath) + " for " + playerId + ": " + ex.GetType().Name + ": " + ex.Message));
				}
			}
			return null;
		}

		private InventorySnapshot FindUniqueByCharacterName(string characterName)
		{
			InventorySnapshot inventorySnapshot = null;
			string[] files = Directory.GetFiles(_players, "*.json");
			for (int i = 0; i < files.Length; i++)
			{
				string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(files[i]);
				if (string.IsNullOrWhiteSpace(fileNameWithoutExtension) || fileNameWithoutExtension.IndexOf(".bak", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					continue;
				}
				InventorySnapshot inventorySnapshot2;
				using (AcquirePlayerLock(fileNameWithoutExtension))
				{
					inventorySnapshot2 = LoadUnsafe(fileNameWithoutExtension);
				}
				if (inventorySnapshot2 != null && string.Equals(inventorySnapshot2.CharacterName, characterName, StringComparison.Ordinal))
				{
					if (inventorySnapshot != null && !string.Equals(inventorySnapshot.PlayerId, inventorySnapshot2.PlayerId, StringComparison.Ordinal))
					{
						Plugin.Log.LogWarning((object)("Shared snapshot lookup by character name is ambiguous for " + characterName + "; refusing the fallback lookup."));
						return null;
					}
					inventorySnapshot = inventorySnapshot2;
				}
			}
			return inventorySnapshot;
		}

		private IEnumerable<string> GetCandidatePaths(string playerId)
		{
			yield return GetPlayerPath(playerId);
			for (int index = 1; index <= _backupCount; index++)
			{
				yield return GetBackupPath(playerId, index);
			}
		}

		private string GetPlayerPath(string playerId)
		{
			return Path.Combine(_players, SafePathPart(playerId) + ".json");
		}

		private string GetBackupPath(string playerId, int index)
		{
			return Path.Combine(_players, SafePathPart(playerId) + $".bak{index}.json");
		}

		private string GetLockPath(string playerId)
		{
			return Path.Combine(_players, SafePathPart(playerId) + ".lock");
		}

		private static string SafePathPart(string value)
		{
			if (!string.IsNullOrWhiteSpace(value))
			{
				return value.Replace("..", "_").Replace("/", "_").Replace("\\", "_");
			}
			return "unknown";
		}

		private FileStream AcquirePlayerLock(string playerId)
		{
			return AcquireLock(GetLockPath(playerId));
		}

		private FileStream AcquireLock(string path)
		{
			DateTime dateTime = DateTime.UtcNow.AddSeconds(_lockTimeoutSeconds);
			while (true)
			{
				try
				{
					return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
				}
				catch (IOException) when (DateTime.UtcNow < dateTime)
				{
					Thread.Sleep(25);
				}
			}
		}

		private static string Limit(string value, int maxLength)
		{
			if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
			{
				return value ?? string.Empty;
			}
			return value.Substring(0, maxLength);
		}

		private static void VerifyWritable(string directory)
		{
			string path = Path.Combine(directory, ".catos-write-probe-" + Guid.NewGuid().ToString("N"));
			try
			{
				using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1, FileOptions.WriteThrough);
				fileStream.WriteByte(1);
				fileStream.Flush(flushToDisk: true);
			}
			catch (Exception inner)
			{
				throw new UnauthorizedAccessException("CatosServerInventory cannot write to authority directory '" + directory + "'. Check Linux owner, group, and directory permissions.", inner);
			}
			finally
			{
				TryDelete(path);
			}
		}

		private bool IsUsable(InventorySnapshot snapshot, string playerId)
		{
			if (snapshot == null || snapshot.SchemaVersion != 1 || (!_shared && !string.Equals(snapshot.ServerId, ServerId, StringComparison.Ordinal)) || (_shared && !string.Equals(snapshot.ClusterId, _clusterId, StringComparison.Ordinal)) || !string.Equals(snapshot.PlayerId, playerId, StringComparison.Ordinal) || !SnapshotCanonicalizer.IsContentValid(snapshot))
			{
				return false;
			}
			if (!string.IsNullOrWhiteSpace(snapshot.RecordChecksum))
			{
				return string.Equals(snapshot.RecordChecksum, SnapshotCanonicalizer.ComputeRecordChecksum(snapshot), StringComparison.Ordinal);
			}
			return true;
		}

		private void RotateBackups(string playerId)
		{
			if (_backupCount >= 3)
			{
				MoveIfExists(GetBackupPath(playerId, 2), GetBackupPath(playerId, 3));
			}
			if (_backupCount >= 2)
			{
				MoveIfExists(GetBackupPath(playerId, 1), GetBackupPath(playerId, 2));
			}
		}

		private void ReplaceWithMove(string temp, string path, string playerId)
		{
			if (File.Exists(path))
			{
				File.Copy(path, GetBackupPath(playerId, 1), overwrite: true);
			}
			File.Delete(path);
			File.Move(temp, path);
		}

		private static void MoveIfExists(string source, string destination)
		{
			if (File.Exists(source))
			{
				TryDelete(destination);
				File.Move(source, destination);
			}
		}

		private static void TryDelete(string path)
		{
			try
			{
				if (File.Exists(path))
				{
					File.Delete(path);
				}
			}
			catch
			{
			}
		}

		private ServerIdentity LoadOrCreateIdentity()
		{
			try
			{
				if (File.Exists(_identityPath))
				{
					ServerIdentity serverIdentity = Read<ServerIdentity>(_identityPath);
					if (serverIdentity != null && !string.IsNullOrWhiteSpace(serverIdentity.ServerId))
					{
						if (_shared && (!string.Equals(serverIdentity.ClusterId, _clusterId, StringComparison.Ordinal) || !string.Equals(serverIdentity.NodeId, _nodeId, StringComparison.Ordinal)))
						{
							throw new InvalidOperationException("Shared node identity does not match the configured cluster or node.");
						}
						return serverIdentity;
					}
				}
			}
			catch (InvalidOperationException)
			{
				throw;
			}
			catch (Exception ex2)
			{
				Plugin.Log.LogWarning((object)("Server identity could not be read; creating a new one: " + ex2.Message));
			}
			ServerIdentity serverIdentity2 = new ServerIdentity
			{
				SchemaVersion = 1,
				ServerId = Guid.NewGuid().ToString("N"),
				CreatedUtc = DateTime.UtcNow.ToString("O"),
				ClusterId = ClusterId,
				NodeId = NodeId
			};
			WriteDurable(_identityPath + ".tmp", serverIdentity2);
			TryDelete(_identityPath);
			File.Move(_identityPath + ".tmp", _identityPath);
			return serverIdentity2;
		}

		private static T Read<T>(string path)
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			using FileStream fileStream = File.OpenRead(path);
			return (T)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).ReadObject((Stream)fileStream);
		}

		private static void WriteDurable<T>(string path, T value)
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			using FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough);
			((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).WriteObject((Stream)fileStream, (object)value);
			fileStream.Flush(flushToDisk: true);
		}
	}
}