Decompiled source of CharactersVault v2.6.1

BepInEx\plugins\CharactersVault\CharactersVault.dll

Decompiled 2 weeks 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.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using CharacterVault.Helpers;
using CharacterVault.Models;
using CharacterVault.Patches;
using CharacterVault.Systems;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Splatform;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace CharacterVault
{
	public static class ModConfig
	{
		public static ConfigEntry<bool> EnforceCharacterBinding { get; private set; }

		public static ConfigEntry<float> AutoSaveIntervalMinutes { get; private set; }

		public static ConfigEntry<string> KickMessageWrongCharacter { get; private set; }

		public static ConfigEntry<float> ProfileSyncTimeoutSeconds { get; private set; }

		public static ConfigEntry<bool> ShowCharacterSelectWarning { get; private set; }

		public static ConfigEntry<bool> VerboseLogging { get; private set; }

		public static void Initialize(ConfigFile cfg)
		{
			EnforceCharacterBinding = cfg.Bind<bool>("Enforcement", "EnforceCharacterBinding", true, "If true, each platform ID may only join with the character name it first registered with.");
			AutoSaveIntervalMinutes = cfg.Bind<float>("ClientSync", "AutoSaveIntervalMinutes", 5f, "How often (in minutes) the client performs a full profile sync. Default: 5.0");
			KickMessageWrongCharacter = cfg.Bind<string>("Messages", "KickMessageWrongCharacter", "Wrong Character", "Message sent to players kicked for using the wrong character.");
			ProfileSyncTimeoutSeconds = cfg.Bind<float>("ClientSync", "ProfileSyncTimeoutSeconds", 15f, "How long (seconds) the client waits for the server to send its profile data on join. If the server does not respond in time, the client disconnects. Default: 15.0");
			ShowCharacterSelectWarning = cfg.Bind<bool>("UI", "ShowCharacterSelectWarning", true, "Show a warning banner on the character selection screen reminding players that existing characters will be wiped on join.");
			VerboseLogging = cfg.Bind<bool>("Debug", "VerboseLogging", false, "Enable extra debug logging to the BepInEx console/log file.");
			ClearOrphanedEntries(cfg);
		}

		private static void ClearOrphanedEntries(ConfigFile cfg)
		{
			try
			{
				PropertyInfo property = typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				Dictionary<ConfigDefinition, string> dictionary = null;
				if (property != null)
				{
					dictionary = property.GetValue(cfg) as Dictionary<ConfigDefinition, string>;
				}
				if (dictionary == null)
				{
					dictionary = (typeof(ConfigFile).GetField("<OrphanedEntries>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic) ?? typeof(ConfigFile).GetField("_orphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic))?.GetValue(cfg) as Dictionary<ConfigDefinition, string>;
				}
				if (dictionary != null && dictionary.Count > 0)
				{
					Plugin.Log.LogInfo((object)string.Format("[{0}] Removing {1} stale/orphaned config entry/entries from {2}...", "CharactersVault", dictionary.Count, cfg.ConfigFilePath));
					dictionary.Clear();
					cfg.Save();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[CharactersVault] Could not clean orphaned config entries: " + ex.Message));
			}
		}
	}
	[BepInPlugin("com.charactervault.valheim", "CharactersVault", "2.6.1")]
	public class Plugin : BaseUnityPlugin
	{
		public const string ModGuid = "com.charactervault.valheim";

		public const string ModName = "CharactersVault";

		public const string ModVersion = "2.6.1";

		private Harmony? _harmony;

		public static Plugin Instance { get; private set; }

		public static ManualLogSource Log { get; private set; }

		private void Awake()
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			try
			{
				Log.LogInfo((object)"══════════════════════════════════════════");
				Log.LogInfo((object)"  CharactersVault v2.6.1 loading...");
				Log.LogInfo((object)"══════════════════════════════════════════");
				ModConfig.Initialize(((BaseUnityPlugin)this).Config);
				DataStore.Initialize();
				BindingManager.Load();
				_harmony = new Harmony("com.charactervault.valheim");
				_harmony.PatchAll();
				Log.LogInfo((object)"[CharactersVault] All Harmony patches applied.");
				Log.LogInfo((object)"[CharactersVault] Loaded successfully. Waiting for network initialization...");
				NetworkManager.Initialize();
				ClientSyncManager.Initialize();
			}
			catch (Exception arg)
			{
				Log.LogError((object)string.Format("[{0}] FATAL: Failed to initialize — {1}", "CharactersVault", arg));
			}
		}

		private void Start()
		{
		}

		private void OnDestroy()
		{
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			Log.LogInfo((object)"[CharactersVault] Unloaded.");
		}
	}
}
namespace CharacterVault.Systems
{
	public static class AdminCommandHandler
	{
		public const string RpcAdminCommand = "CharacterVault_AdminCmd";

		public const string RpcAdminResponse = "CharacterVault_AdminResp";

		public static void RegisterRPCs()
		{
			ZRoutedRpc.instance.Register<string>("CharacterVault_AdminCmd", (Action<long, string>)RPC_AdminCommand);
			ZRoutedRpc.instance.Register<string>("CharacterVault_AdminResp", (Action<long, string>)RPC_AdminResponse);
		}

		public static void SendAdminCommand(string text)
		{
			if (!string.IsNullOrWhiteSpace(text))
			{
				if (!text.StartsWith("/", StringComparison.OrdinalIgnoreCase))
				{
					text = "/" + text;
				}
				if ((Object)(object)ZNet.instance == (Object)null)
				{
					DisplayResponse("<color=#FFCC00>[CharactersVault]</color> Not connected to a server.");
					return;
				}
				if (ZNet.instance.IsServer())
				{
					DisplayResponse(ExecuteCommand(text));
					return;
				}
				Plugin.Log.LogInfo((object)("[AdminCmd] Sending admin command to server: " + text));
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "CharacterVault_AdminCmd", new object[1] { text });
			}
		}

		private static void RPC_AdminCommand(long sender, string text)
		{
			if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
			{
				return;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(sender);
			if (peer != null)
			{
				string playerId = ZNetHelper.GetPlayerId(peer);
				if (!ZNetHelper.IsAdmin(peer))
				{
					ManualLogSource log = Plugin.Log;
					string[] obj = new string[6] { "[AdminCmd] Non-admin ", playerId, " (host: ", null, null, null };
					ISocket socket = peer.m_socket;
					obj[3] = ((socket != null) ? socket.GetHostName() : null);
					obj[4] = ") tried to run: ";
					obj[5] = text;
					log.LogWarning((object)string.Concat(obj));
					ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_AdminResp", new object[1] { "<color=#FF4444>[CharactersVault] Access denied: You are not listed in the server's adminlist.txt.</color>" });
				}
				else
				{
					Plugin.Log.LogInfo((object)("[AdminCmd] Executing '" + text + "' for admin " + playerId));
					string text2 = ExecuteCommand(text);
					ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_AdminResp", new object[1] { text2 });
				}
			}
		}

		private static void RPC_AdminResponse(long sender, string response)
		{
			DisplayResponse(response);
		}

		private static void DisplayResponse(string response)
		{
			Plugin.Log.LogInfo((object)("[AdminCmd] Response:\n" + response));
			if ((Object)(object)Chat.instance != (Object)null)
			{
				((Terminal)Chat.instance).AddString(response);
			}
		}

		private static string ExecuteCommand(string text)
		{
			string text2 = text;
			if (text2.StartsWith("/vault", StringComparison.OrdinalIgnoreCase))
			{
				text2 = text2.Substring(6).Trim();
			}
			else if (text2.StartsWith("vault", StringComparison.OrdinalIgnoreCase))
			{
				text2 = text2.Substring(5).Trim();
			}
			else if (text2.StartsWith("/cv", StringComparison.OrdinalIgnoreCase))
			{
				text2 = text2.Substring(3).Trim();
			}
			else if (text2.StartsWith("cv", StringComparison.OrdinalIgnoreCase))
			{
				text2 = text2.Substring(2).Trim();
			}
			string[] array = text2.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length == 0)
			{
				return PrintHelp();
			}
			string text3 = array[0].ToLowerInvariant();
			switch (text3)
			{
			case "wipe":
			case "remove":
			case "delete":
			case "unbind":
			case "reset":
				return CmdReset(array);
			case "list":
				return CmdList();
			case "status":
				return CmdStatus(array);
			case "help":
				return PrintHelp();
			default:
				return "<color=#FFCC00>[CharactersVault]</color> Unknown command '" + text3 + "'. Type <color=#33FF33>/cv help</color> for a list.";
			}
		}

		private static string CmdReset(string[] tokens)
		{
			if (tokens.Length < 2)
			{
				return "<color=#FFCC00>[CharactersVault]</color> Missing player ID. Example: <color=#33FF33>/cv reset Steam_76561198XXXXXXXXX</color>";
			}
			string text = tokens[1];
			bool num = DataStore.WipePlayerData(text);
			BindingManager.Load();
			if (num)
			{
				ZNetPeer val = ZNetHelper.FindPeerByPlayerId(text);
				if (val != null)
				{
					NetworkManager.Instance?.RejectPeer(val, "Your character was reset by an administrator. You may reconnect with a fresh character.");
				}
				return "<color=#33FF33>[CharactersVault]</color> Reset player " + text + ". Character lock and progression wiped. They may register a fresh character on next join.";
			}
			return "<color=#FFCC00>[CharactersVault]</color> No data found for " + text + " — nothing to reset.";
		}

		private static string CmdList()
		{
			IReadOnlyDictionary<string, CharacterRecord> all = BindingManager.GetAll();
			if (all.Count == 0)
			{
				return "<color=#FFCC00>[CharactersVault]</color> No character bindings registered.";
			}
			StringBuilder stringBuilder = new StringBuilder($"<color=#33CCFF>[CharactersVault]</color> Character Bindings ({all.Count}):\n");
			foreach (KeyValuePair<string, CharacterRecord> item in all)
			{
				stringBuilder.AppendLine($"  <color=#FFCC00>{item.Key}</color> → '{item.Value.CharacterName}' (since {item.Value.RegisteredAt:yyyy-MM-dd})");
			}
			return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
		}

		private static string CmdStatus(string[] tokens)
		{
			if (tokens.Length < 2)
			{
				return "<color=#FFCC00>[CharactersVault]</color> Missing player ID. Example: <color=#33FF33>/cv status Steam_76561198XXXXXXXXX</color>";
			}
			string text = tokens[1];
			string registeredName = BindingManager.GetRegisteredName(text);
			PlayerSnapshot playerSnapshot = DataStore.LoadSnapshot(text);
			StringBuilder stringBuilder = new StringBuilder("<color=#33CCFF>[CharactersVault]</color> Status for " + text + ":\n");
			stringBuilder.AppendLine("  Binding:   " + ((registeredName != null) ? ("'" + registeredName + "'") : "Not registered"));
			stringBuilder.Append("  Snapshot:  " + ((playerSnapshot != null) ? $"Taken {playerSnapshot.SnapshotTime:yyyy-MM-dd HH:mm} UTC" : "None"));
			return stringBuilder.ToString();
		}

		private static string PrintHelp()
		{
			return "<color=#33CCFF>[CharactersVault]</color> Admin Commands:\n  <color=#33FF33>/cv list</color> — Show all bindings\n  <color=#33FF33>/cv status [playerId]</color> — Show binding + snapshot info\n  <color=#33FF33>/cv reset [playerId]</color> — Reset player progression & allow new character registration (aliases: /cv wipe, /cv remove)\n  <color=#33FF33>/cv help</color> — This message";
		}
	}
	public static class BindingManager
	{
		private static Dictionary<string, CharacterRecord> _bindings = new Dictionary<string, CharacterRecord>();

		public static void Load()
		{
			_bindings = DataStore.LoadBindings();
			Plugin.Log.LogInfo((object)$"[BindingManager] Loaded {_bindings.Count} character binding(s).");
		}

		private static void Save()
		{
			DataStore.SaveBindings(_bindings);
		}

		public static bool IsRegistered(string playerId)
		{
			return _bindings.ContainsKey(playerId);
		}

		public static string? GetRegisteredName(string playerId)
		{
			if (!_bindings.TryGetValue(playerId, out CharacterRecord value))
			{
				return null;
			}
			return value.CharacterName;
		}

		public static void Register(string playerId, string characterName)
		{
			_bindings[playerId] = new CharacterRecord
			{
				PlayerId = playerId,
				CharacterName = characterName,
				RegisteredAt = DateTime.UtcNow,
				LastSeenAt = DateTime.UtcNow
			};
			Save();
			Plugin.Log.LogInfo((object)("[BindingManager] Registered " + playerId + " → '" + characterName + "'"));
		}

		public static void RecordJoin(string playerId)
		{
			if (_bindings.TryGetValue(playerId, out CharacterRecord value))
			{
				value.LastSeenAt = DateTime.UtcNow;
				Save();
			}
		}

		public static bool RemoveBinding(string playerId)
		{
			DataStore.DeleteSnapshot(playerId);
			if (!_bindings.Remove(playerId))
			{
				return false;
			}
			Save();
			Plugin.Log.LogInfo((object)("[BindingManager] Removed binding and snapshot for " + playerId));
			return true;
		}

		public static IReadOnlyDictionary<string, CharacterRecord> GetAll()
		{
			return new Dictionary<string, CharacterRecord>(_bindings);
		}
	}
	public class ClientSyncManager : MonoBehaviour
	{
		private const float DebounceDelaySeconds = 2.5f;

		private Coroutine? _syncCoroutine;

		private bool _hasPendingSnapshot;

		private float _pendingSnapshotTime;

		private string _pendingReason = string.Empty;

		public static ClientSyncManager Instance { get; private set; }

		public static void Initialize()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Expected O, but got Unknown
			GameObject val = new GameObject("CharacterVault_ClientSyncManager");
			Instance = val.AddComponent<ClientSyncManager>();
			Object.DontDestroyOnLoad((Object)val);
		}

		private void Start()
		{
			_syncCoroutine = ((MonoBehaviour)this).StartCoroutine(PeriodicSyncCoroutine());
		}

		private void OnDestroy()
		{
			if (_syncCoroutine != null)
			{
				((MonoBehaviour)this).StopCoroutine(_syncCoroutine);
			}
		}

		private void Update()
		{
			if (_hasPendingSnapshot && Time.unscaledTime >= _pendingSnapshotTime)
			{
				ExecuteSnapshotUpload(_pendingReason);
			}
		}

		public void QueueSnapshotUpdate(string reason)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !ClientProfilePatches.IsWaitingForProfile() && !ClientProfilePatches.IsInitializingFirstJoin())
			{
				_hasPendingSnapshot = true;
				_pendingReason = reason;
				_pendingSnapshotTime = Time.unscaledTime + 2.5f;
			}
		}

		public void FlushImmediate(string reason)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !ClientProfilePatches.IsWaitingForProfile() && !ClientProfilePatches.IsInitializingFirstJoin())
			{
				ExecuteSnapshotUpload(reason);
			}
		}

		private void ExecuteSnapshotUpload(string reason)
		{
			_hasPendingSnapshot = false;
			try
			{
				byte[] array = ClientProfilePatches.CaptureLivePlayerData();
				if (array.Length != 0)
				{
					Plugin.Log.LogInfo((object)("[ClientSyncManager] Uploading live player-data checkpoint: " + reason + "."));
					NetworkManager.Instance.SendProfileDataToServer(array, isPlayerData: true);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ClientSyncManager] Failed to upload live player-data checkpoint: {arg}");
			}
		}

		private IEnumerator PeriodicSyncCoroutine()
		{
			while (true)
			{
				float num = ModConfig.AutoSaveIntervalMinutes.Value * 60f;
				yield return (object)new WaitForSeconds(num);
				try
				{
					if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !((Object)(object)Game.instance == (Object)null))
					{
						Plugin.Log.LogInfo((object)"[ClientSyncManager] Running periodic profile sync to server.");
						ClientProfilePatches.SafeSavePlayerProfile(setLogoutPoint: true);
					}
				}
				catch (Exception arg)
				{
					Plugin.Log.LogError((object)$"[ClientSyncManager] Exception during periodic sync: {arg}");
				}
			}
		}
	}
	public static class ConnectionRejectionManager
	{
		private static string? _reason;

		public static void SetReason(string reason)
		{
			_reason = reason;
		}

		public static string? ConsumeReason()
		{
			string? reason = _reason;
			_reason = null;
			return reason;
		}
	}
	public static class DataStore
	{
		private static string _rootDir = string.Empty;

		private static string _snapshotsDir = string.Empty;

		public static string BindingsFilePath => Path.Combine(_rootDir, "bindings.json");

		private static string SnapshotPath(string playerId)
		{
			return Path.Combine(_snapshotsDir, playerId + ".json");
		}

		public static void Initialize()
		{
			_rootDir = Path.Combine(Paths.ConfigPath, "CharacterVault");
			_snapshotsDir = Path.Combine(_rootDir, "snapshots");
			Directory.CreateDirectory(_rootDir);
			Directory.CreateDirectory(_snapshotsDir);
			Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Root Data Directory: " + _rootDir));
			Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Snapshots Directory: " + _snapshotsDir));
		}

		public static Dictionary<string, CharacterRecord> LoadBindings()
		{
			try
			{
				if (!File.Exists(BindingsFilePath))
				{
					return new Dictionary<string, CharacterRecord>();
				}
				return SimpleJson.DeserializeObject<Dictionary<string, CharacterRecord>>(File.ReadAllText(BindingsFilePath)) ?? new Dictionary<string, CharacterRecord>();
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load bindings from '" + BindingsFilePath + "': " + ex.Message));
				return new Dictionary<string, CharacterRecord>();
			}
		}

		public static void SaveBindings(Dictionary<string, CharacterRecord> bindings)
		{
			try
			{
				string content = SimpleJson.SerializeObject(bindings);
				WriteAllTextAtomically(BindingsFilePath, content);
				Plugin.Log.LogInfo((object)$"[CharacterVault :: DataStore] Saved {bindings.Count} binding(s) to '{BindingsFilePath}'");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to save bindings: " + ex.Message));
			}
		}

		public static PlayerSnapshot? LoadSnapshot(string playerId)
		{
			string text = SnapshotPath(playerId);
			try
			{
				if (!File.Exists(text))
				{
					Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] No snapshot file found at '" + text + "'"));
					return null;
				}
				PlayerSnapshot result = SimpleJson.DeserializeObject<PlayerSnapshot>(File.ReadAllText(text));
				Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Loaded snapshot file for platform ID " + playerId + " from '" + text + "'"));
				return result;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load snapshot for platform ID " + playerId + ": " + ex.Message));
				return null;
			}
		}

		public static void SaveSnapshot(PlayerSnapshot snapshot)
		{
			try
			{
				string text = SnapshotPath(snapshot.PlayerId);
				string content = SimpleJson.SerializeObject(snapshot);
				WriteAllTextAtomically(text, content);
				Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Saved snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "') -> '" + text + "'"));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to save snapshot for platform ID " + snapshot.PlayerId + ": " + ex.Message));
			}
		}

		public static bool DeleteSnapshot(string playerId)
		{
			string path = SnapshotPath(playerId);
			if (File.Exists(path))
			{
				try
				{
					File.Delete(path);
					Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Deleted snapshot file for platform ID " + playerId + "."));
					return true;
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to delete snapshot for " + playerId + ": " + ex.Message));
				}
			}
			return false;
		}

		public static bool WipePlayerData(string playerId)
		{
			bool result = DeleteSnapshot(playerId);
			Dictionary<string, CharacterRecord> dictionary = LoadBindings();
			if (dictionary.ContainsKey(playerId))
			{
				dictionary.Remove(playerId);
				SaveBindings(dictionary);
				Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Wipe: removed character binding for platform ID " + playerId + "."));
				result = true;
			}
			return result;
		}

		private static void WriteAllTextAtomically(string path, string content)
		{
			string text = $"{path}.{Guid.NewGuid():N}.tmp";
			try
			{
				File.WriteAllText(text, content);
				if (File.Exists(path))
				{
					File.Replace(text, path, null);
				}
				else
				{
					File.Move(text, path);
				}
			}
			finally
			{
				if (File.Exists(text))
				{
					File.Delete(text);
				}
			}
		}
	}
	public class NetworkManager : MonoBehaviour
	{
		private sealed class IncomingTransfer
		{
			public int TotalChunks { get; }

			public Dictionary<int, byte[]> Chunks { get; } = new Dictionary<int, byte[]>();

			public int TotalBytes { get; set; }

			public float LastUpdated { get; set; }

			public IncomingTransfer(int totalChunks)
			{
				TotalChunks = totalChunks;
				LastUpdated = Time.unscaledTime;
			}
		}

		private const string RpcHandshake = "CharacterVault_Handshake";

		private const string RpcProfileData = "CharacterVault_ProfileData";

		private const string RpcSaveProfile = "CharacterVault_SaveProfile";

		private const string RpcSaveProfileChunk = "CharacterVault_SaveProfileChunk";

		private const string RpcProfileDataChunk = "CharacterVault_ProfileDataChunk";

		private const string RpcKickReason = "CharacterVault_KickReason";

		private const int ChunkSize = 512000;

		private const int MaxProfileBytes = 67108864;

		private const float IncomingTransferTimeoutSeconds = 120f;

		private readonly HashSet<long> _handshakeCompleted = new HashSet<long>();

		private readonly Dictionary<long, IncomingTransfer> _incomingChunks = new Dictionary<long, IncomingTransfer>();

		private float _nextIncomingTransferCleanup;

		public static NetworkManager Instance { get; private set; }

		public static void Initialize()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Expected O, but got Unknown
			GameObject val = new GameObject("CharacterVault_NetworkManager");
			Instance = val.AddComponent<NetworkManager>();
			Object.DontDestroyOnLoad((Object)val);
			Plugin.Log.LogInfo((object)"[CharacterVault :: Network] NetworkManager initialized.");
		}

		public void RegisterRPCs()
		{
			ZRoutedRpc.instance.Register<string>("CharacterVault_Handshake", (Action<long, string>)RPC_Handshake);
			ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("CharacterVault_ProfileDataChunk", (Method<int, int, bool, bool, ZPackage>)RPC_ProfileDataChunk);
			ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("CharacterVault_SaveProfileChunk", (Method<int, int, bool, bool, ZPackage>)RPC_SaveProfileChunk);
			ZRoutedRpc.instance.Register<string>("CharacterVault_KickReason", (Action<long, string>)RPC_KickReason);
			AdminCommandHandler.RegisterRPCs();
			Plugin.Log.LogInfo((object)"[CharacterVault :: Network] RPC handlers registered.");
		}

		private void RPC_KickReason(long sender, string reason)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer())
			{
				ConnectionRejectionManager.SetReason(reason);
				Plugin.Log.LogWarning((object)("[CharacterVault :: Network] Server rejection reason: " + reason));
			}
		}

		public void RejectPeer(ZNetPeer peer, string reason)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Rejecting peer {peer.m_uid} (platform ID {ZNetHelper.GetPlayerId(peer)}): {reason}");
				ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "CharacterVault_KickReason", new object[1] { reason });
				((MonoBehaviour)this).StartCoroutine(DisconnectRejectedPeer(peer));
			}
		}

		private IEnumerator DisconnectRejectedPeer(ZNetPeer peer)
		{
			yield return (object)new WaitForSecondsRealtime(1f);
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.GetPeer(peer.m_uid) == peer)
			{
				peer.m_rpc.Invoke("Error", new object[1] { 12 });
				ZNet.instance.Disconnect(peer);
			}
		}

		private void RPC_Handshake(long sender, string version)
		{
			if (ZNet.instance.IsServer())
			{
				if (version == "request")
				{
					return;
				}
				if (version != "2.6.1")
				{
					Plugin.Log.LogWarning((object)string.Format("[CharacterVault :: Network] Peer {0} wrong mod version: '{1}' (expected '{2}'). KICKING.", sender, version, "2.6.1"));
					ZNetPeer peer = ZNet.instance.GetPeer(sender);
					if (peer != null)
					{
						RejectPeer(peer, "CharactersVault version mismatch: client has v" + version + ", server requires v2.6.1");
					}
					return;
				}
				Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} COMPLETED HANDSHAKE (v{version}).");
				_handshakeCompleted.Add(sender);
				ZNetPeer peer2 = ZNet.instance.GetPeer(sender);
				if (peer2 == null)
				{
					Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Peer {sender} null after handshake!");
					return;
				}
				string playerId = ZNetHelper.GetPlayerId(peer2);
				Plugin.Log.LogInfo((object)("[CharacterVault :: Network] Checking snapshot store for platform ID " + playerId + " ('" + peer2.m_playerName + "')..."));
				PlayerSnapshot snapshot = SnapshotManager.GetSnapshot(playerId);
				bool flag = snapshot != null && snapshot.HasData && string.Equals(snapshot.CharacterName, peer2.m_playerName, StringComparison.OrdinalIgnoreCase);
				byte[] array;
				if (flag)
				{
					array = snapshot.GetProfileBytes();
					Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> sending existing stored profile for '{snapshot.CharacterName}' ({array.Length} bytes).");
				}
				else
				{
					if (snapshot != null && !string.IsNullOrEmpty(snapshot.CharacterName) && !string.Equals(snapshot.CharacterName, peer2.m_playerName, StringComparison.OrdinalIgnoreCase))
					{
						Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> stored snapshot is for character '{snapshot.CharacterName}', but player joined with '{peer2.m_playerName}'. Discarding old snapshot.");
						SnapshotManager.DeleteSnapshot(playerId);
					}
					Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> no valid snapshot for '{peer2.m_playerName}' (first join). Requesting client-side clean initialization.");
					array = Array.Empty<byte>();
				}
				SendProfileDataToClient(sender, array, flag && snapshot.IsPlayerData, !flag);
			}
			else if (version == "request")
			{
				Plugin.Log.LogInfo((object)"[CharacterVault :: Network] Server requested handshake. Replying with version '2.6.1'.");
				ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_Handshake", new object[1] { "2.6.1" });
			}
		}

		public void SendHandshakeRequest(ZNetPeer peer)
		{
			Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Initiating handshake with peer {peer.m_uid} (platform ID {peer.m_socket.GetHostName()})...");
			ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "CharacterVault_Handshake", new object[1] { "request" });
			((MonoBehaviour)this).StartCoroutine(HandshakeTimeout(peer));
		}

		private IEnumerator HandshakeTimeout(ZNetPeer peer)
		{
			float timeout = ModConfig.ProfileSyncTimeoutSeconds.Value;
			yield return (object)new WaitForSecondsRealtime(timeout);
			if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.GetPeer(peer.m_uid) != null && !_handshakeCompleted.Contains(peer.m_uid))
			{
				Plugin.Log.LogWarning((object)($"[CharacterVault :: Network] Peer {peer.m_uid} TIMED OUT after {timeout}s waiting for handshake. " + "Client mod not installed or incompatible. KICKING PEER."));
				RejectPeer(peer, "CharactersVault handshake timeout: server did not receive client handshake reply.");
			}
		}

		public void OnPeerDisconnected(long peerId)
		{
			_incomingChunks.Remove(peerId);
			if (_handshakeCompleted.Remove(peerId))
			{
				Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Cleaned up handshake tracking for disconnected peer {peerId}.");
			}
		}

		private void SendProfileDataToClient(long peerId, byte[] data, bool isPlayerData, bool isFirstJoin)
		{
			Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Transmitting {data.Length} profile bytes to client peer {peerId}...");
			SendChunks(peerId, "CharacterVault_ProfileDataChunk", data, isPlayerData, isFirstJoin);
		}

		public void SendProfileDataToServer(byte[] data, bool isPlayerData = false)
		{
			Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Transmitting {data.Length} profile bytes to server...");
			SendChunks(0L, "CharacterVault_SaveProfileChunk", data, isPlayerData, isFirstJoin: false);
		}

		private void SendChunks(long target, string rpcName, byte[] data, bool isPlayerData, bool isFirstJoin)
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Expected O, but got Unknown
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Expected O, but got Unknown
			int num = Mathf.CeilToInt((float)data.Length / 512000f);
			if (num == 0)
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(target, rpcName, new object[5]
				{
					0,
					0,
					isPlayerData,
					isFirstJoin,
					(object)new ZPackage()
				});
				return;
			}
			for (int i = 0; i < num; i++)
			{
				int num2 = Mathf.Min(512000, data.Length - i * 512000);
				byte[] array = new byte[num2];
				Array.Copy(data, i * 512000, array, 0, num2);
				ZRoutedRpc.instance.InvokeRoutedRPC(target, rpcName, new object[5]
				{
					num,
					i,
					isPlayerData,
					isFirstJoin,
					(object)new ZPackage(array)
				});
			}
		}

		private void RPC_ProfileDataChunk(long sender, int totalChunks, int chunkIndex, bool isPlayerData, bool isFirstJoin, ZPackage chunk)
		{
			if (!ZNet.instance.IsServer())
			{
				byte[] array = ProcessIncomingChunk(sender, totalChunks, chunkIndex, chunk.GetArray());
				if (array != null)
				{
					Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] CLIENT: Full profile data reassembled ({array.Length} bytes). Passing to ClientProfilePatches.");
					ClientProfilePatches.ReceiveServerProfile(array, isPlayerData, isFirstJoin);
				}
			}
		}

		private void RPC_SaveProfileChunk(long sender, int totalChunks, int chunkIndex, bool isPlayerData, bool isFirstJoin, ZPackage chunk)
		{
			if (!ZNet.instance.IsServer())
			{
				return;
			}
			byte[] array = ProcessIncomingChunk(sender, totalChunks, chunkIndex, chunk.GetArray());
			if (array == null)
			{
				return;
			}
			ZNetPeer peer = ZNet.instance.GetPeer(sender);
			if (peer == null)
			{
				return;
			}
			string playerId = ZNetHelper.GetPlayerId(peer);
			if (ModConfig.EnforceCharacterBinding.Value)
			{
				string registeredName = BindingManager.GetRegisteredName(playerId);
				if (string.IsNullOrEmpty(registeredName) || !string.Equals(registeredName, peer.m_playerName, StringComparison.OrdinalIgnoreCase))
				{
					Plugin.Log.LogWarning((object)string.Format("[CharacterVault :: Network] Ignored profile upload from peer {0} (platform ID {1}, character '{2}') because they are not currently bound to this character (registered: '{3}').", sender, playerId, peer.m_playerName, registeredName ?? "none"));
					return;
				}
			}
			Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Server received full profile upload ({array.Length} bytes) from peer {sender} (platform ID {playerId}, Character '{peer.m_playerName}')");
			SnapshotManager.SaveSnapshot(SnapshotManager.CreateSnapshot(playerId, peer.m_playerName, array, isPlayerData));
		}

		private byte[]? ProcessIncomingChunk(long sender, int totalChunks, int chunkIndex, byte[] chunk)
		{
			if (totalChunks == 0)
			{
				return Array.Empty<byte>();
			}
			int num = Mathf.CeilToInt(131.072f);
			if (totalChunks < 1 || totalChunks > num || chunkIndex < 0 || chunkIndex >= totalChunks || chunk.Length > 512000)
			{
				Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Discarded malformed chunk transfer from peer {sender}.");
				_incomingChunks.Remove(sender);
				return null;
			}
			if (!_incomingChunks.TryGetValue(sender, out IncomingTransfer value) || value.TotalChunks != totalChunks)
			{
				value = new IncomingTransfer(totalChunks);
				_incomingChunks[sender] = value;
			}
			value.LastUpdated = Time.unscaledTime;
			if (value.Chunks.TryGetValue(chunkIndex, out byte[] value2))
			{
				value.TotalBytes -= value2.Length;
			}
			value.TotalBytes += chunk.Length;
			if (value.TotalBytes > 67108864)
			{
				Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Discarded oversized profile transfer from peer {sender}.");
				_incomingChunks.Remove(sender);
				return null;
			}
			value.Chunks[chunkIndex] = chunk;
			if (value.Chunks.Count == totalChunks)
			{
				using (MemoryStream memoryStream = new MemoryStream(value.TotalBytes))
				{
					for (int i = 0; i < totalChunks; i++)
					{
						if (!value.Chunks.TryGetValue(i, out byte[] value3))
						{
							_incomingChunks.Remove(sender);
							return null;
						}
						memoryStream.Write(value3, 0, value3.Length);
					}
					_incomingChunks.Remove(sender);
					return memoryStream.ToArray();
				}
			}
			return null;
		}

		private void Update()
		{
			if (Time.unscaledTime < _nextIncomingTransferCleanup)
			{
				return;
			}
			_nextIncomingTransferCleanup = Time.unscaledTime + 10f;
			List<long> list = new List<long>();
			foreach (KeyValuePair<long, IncomingTransfer> incomingChunk in _incomingChunks)
			{
				if (Time.unscaledTime - incomingChunk.Value.LastUpdated > 120f)
				{
					list.Add(incomingChunk.Key);
				}
			}
			foreach (long item in list)
			{
				_incomingChunks.Remove(item);
				Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Discarded incomplete profile transfer from peer {item} after timeout.");
			}
		}
	}
	public static class SnapshotManager
	{
		public static PlayerSnapshot CreateSnapshot(string playerId, string characterName, byte[] profileBytes, bool isPlayerData)
		{
			Plugin.Log.LogInfo((object)$"[CharacterVault :: Snapshot] Creating snapshot for platform ID {playerId} ('{characterName}'), raw bytes length: {((profileBytes != null) ? profileBytes.Length : 0)}");
			return new PlayerSnapshot
			{
				PlayerId = playerId,
				CharacterName = characterName,
				SnapshotTime = DateTime.UtcNow,
				IsPlayerData = isPlayerData,
				ProfileDataBase64 = ((profileBytes != null && profileBytes.Length != 0) ? Convert.ToBase64String(profileBytes) : string.Empty)
			};
		}

		public static PlayerSnapshot? GetSnapshot(string playerId)
		{
			PlayerSnapshot playerSnapshot = DataStore.LoadSnapshot(playerId);
			if (playerSnapshot != null && playerSnapshot.HasData)
			{
				Plugin.Log.LogInfo((object)$"[CharacterVault :: Snapshot] Found existing snapshot for platform ID {playerId} ('{playerSnapshot.CharacterName}'), created: {playerSnapshot.SnapshotTime}");
			}
			else
			{
				Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] No existing snapshot found for platform ID " + playerId + "."));
			}
			return playerSnapshot;
		}

		public static void SaveSnapshot(PlayerSnapshot snapshot)
		{
			Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] Saving snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "')..."));
			DataStore.SaveSnapshot(snapshot);
		}

		public static bool DeleteSnapshot(string playerId)
		{
			Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] Deleting snapshot for platform ID " + playerId + "..."));
			return DataStore.DeleteSnapshot(playerId);
		}
	}
}
namespace CharacterVault.Patches
{
	[HarmonyPatch(typeof(Terminal), "TryRunCommand")]
	public static class Terminal_TryRunCommand_Patch
	{
		[HarmonyPrefix]
		public static bool Prefix(Terminal __instance, string text)
		{
			try
			{
				if (string.IsNullOrWhiteSpace(text))
				{
					return true;
				}
				string text2 = text.Trim();
				if (text2.StartsWith("cv ", StringComparison.OrdinalIgnoreCase) || text2.Equals("cv", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("/cv ", StringComparison.OrdinalIgnoreCase) || text2.Equals("/cv", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("vault ", StringComparison.OrdinalIgnoreCase) || text2.Equals("vault", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("/vault ", StringComparison.OrdinalIgnoreCase) || text2.Equals("/vault", StringComparison.OrdinalIgnoreCase))
				{
					AdminCommandHandler.SendAdminCommand(text2);
					return false;
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[TerminalPatch] Error in TryRunCommand prefix: {arg}");
			}
			return true;
		}
	}
	public static class ClientProfilePatches
	{
		private static byte[]? _serverProfileData;

		private static bool _waitingForProfile;

		internal static bool FirstJoinInitializationPending;

		internal static bool IsFirstJoinInitializationActive;

		public static void ReceiveServerProfile(byte[] profileData, bool isPlayerData, bool isFirstJoin)
		{
			Plugin.Log.LogInfo((object)$"[ClientProfilePatches] Received {profileData.Length} bytes from server.");
			if (isFirstJoin)
			{
				FirstJoinInitializationPending = true;
				_waitingForProfile = false;
				Plugin.Log.LogInfo((object)"[ClientProfilePatches] First join confirmed. Preserving appearance and clearing gameplay state after local profile load.");
				if ((Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile() != null && (Object)(object)Player.m_localPlayer != (Object)null)
				{
					try
					{
						Traverse.Create((object)Game.instance.GetPlayerProfile()).Method("LoadPlayerData", new object[1] { Player.m_localPlayer }).GetValue();
						Plugin.Log.LogInfo((object)"[ClientProfilePatches] Triggered initial LoadPlayerData for live Player on first join.");
						return;
					}
					catch (Exception arg)
					{
						Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to trigger LoadPlayerData on first join: {arg}");
						return;
					}
				}
				return;
			}
			if (profileData == null || profileData.Length == 0)
			{
				Plugin.Log.LogError((object)"[ClientProfilePatches] Server returned an empty profile. Keeping local profile blocked.");
				return;
			}
			_serverProfileData = profileData;
			if ((Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile() != null)
			{
				PlayerProfile playerProfile = Game.instance.GetPlayerProfile();
				string name = playerProfile.GetName();
				if (isPlayerData ? ApplyServerPlayerData(playerProfile, profileData) : WriteServerDataToDisk(playerProfile, profileData))
				{
					if (!isPlayerData)
					{
						try
						{
							Traverse.Create((object)playerProfile).Method("LoadPlayerFromDisk", Array.Empty<object>()).GetValue();
							playerProfile.SetName(name);
							Plugin.Log.LogInfo((object)"[ClientProfilePatches] Reloaded PlayerProfile memory from server data.");
						}
						catch (Exception ex)
						{
							Plugin.Log.LogError((object)("[ClientProfilePatches] Failed to reload PlayerProfile from disk: " + ex.Message));
						}
					}
					_waitingForProfile = false;
					if ((Object)(object)Player.m_localPlayer != (Object)null)
					{
						try
						{
							Traverse.Create((object)playerProfile).Method("LoadPlayerData", new object[1] { Player.m_localPlayer }).GetValue();
							Plugin.Log.LogInfo((object)"[ClientProfilePatches] Re-applied server profile data to live Player instance!");
						}
						catch (Exception ex2)
						{
							Plugin.Log.LogError((object)("[ClientProfilePatches] Failed to apply server profile to live Player: " + ex2.Message));
						}
					}
				}
			}
			_waitingForProfile = false;
		}

		public static void ExpectServerProfile()
		{
			_serverProfileData = null;
			_waitingForProfile = true;
			Plugin.Log.LogInfo((object)"[ClientProfilePatches] Waiting for server profile data...");
		}

		public static bool IsWaitingForProfile()
		{
			return _waitingForProfile;
		}

		public static bool IsInitializingFirstJoin()
		{
			return IsFirstJoinInitializationActive;
		}

		public static byte[]? GetServerProfile()
		{
			return _serverProfileData;
		}

		public static void Reset()
		{
			_serverProfileData = null;
			_waitingForProfile = false;
			FirstJoinInitializationPending = false;
			IsFirstJoinInitializationActive = false;
		}

		public static void SafeSavePlayerProfile(bool setLogoutPoint)
		{
			if ((Object)(object)Game.instance == (Object)null)
			{
				return;
			}
			try
			{
				Game.instance.SavePlayerProfile(setLogoutPoint, false);
			}
			catch (MissingMethodException)
			{
				try
				{
					MethodInfo method = typeof(Game).GetMethod("SavePlayerProfile", new Type[1] { typeof(bool) });
					if (method != null)
					{
						method.Invoke(Game.instance, new object[1] { setLogoutPoint });
						return;
					}
				}
				catch (Exception arg)
				{
					Plugin.Log.LogError((object)$"[ClientProfilePatches] Legacy SavePlayerProfile invocation failed: {arg}");
				}
				throw;
			}
		}

		private static bool WriteServerDataToDisk(PlayerProfile profile, byte[] data)
		{
			//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_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Expected O, but got Unknown
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Invalid comparison between Unknown and I4
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			if (data == null || data.Length == 0)
			{
				Plugin.Log.LogWarning((object)"[WriteServerDataToDisk] Server sent empty profile bytes — skipping disk overwrite.");
				return false;
			}
			try
			{
				string path = profile.GetPath();
				FileSource fileSource = GetFileSource(profile);
				FileWriter val = new FileWriter(path, (CloudStorageFileGrouping)1, (FileHelperType)0, fileSource);
				val.m_binary.Write(data);
				val.Finish();
				if ((int)val.Status != 2)
				{
					throw new IOException($"Valheim failed to write the profile ({val.Status}).");
				}
				Plugin.Log.LogInfo((object)$"[WriteServerDataToDisk] Wrote {data.Length} server bytes to '{path}'.");
				return true;
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[WriteServerDataToDisk] Failed to write server profile to disk: {arg}");
				return false;
			}
		}

		public static byte[] ReadProfileBytes(PlayerProfile profile)
		{
			//IL_0007: 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_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_000f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Expected O, but got Unknown
			string path = profile.GetPath();
			FileSource fileSource = GetFileSource(profile);
			FileReader val = new FileReader(path, fileSource, (FileHelperType)0);
			try
			{
				int count = (int)(val.m_binary.BaseStream.Length - val.m_binary.BaseStream.Position);
				return val.m_binary.ReadBytes(count);
			}
			finally
			{
				val.Dispose();
			}
		}

		public static byte[] CaptureLivePlayerData()
		{
			if ((Object)(object)Game.instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null)
			{
				return Array.Empty<byte>();
			}
			PlayerProfile playerProfile = Game.instance.GetPlayerProfile();
			if (playerProfile == null)
			{
				return Array.Empty<byte>();
			}
			playerProfile.SavePlayerData(Player.m_localPlayer);
			return (byte[])Traverse.Create((object)playerProfile).Field("m_playerData").GetValue();
		}

		private static bool ApplyServerPlayerData(PlayerProfile profile, byte[] data)
		{
			try
			{
				Traverse.Create((object)profile).Field("m_playerData").SetValue((object)data);
				Plugin.Log.LogInfo((object)$"[ClientProfilePatches] Applied {data.Length} bytes of authoritative live player data.");
				return true;
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to apply live player data: {arg}");
				return false;
			}
		}

		private static FileSource GetFileSource(PlayerProfile profile)
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			return (FileSource)Traverse.Create((object)profile).Field("m_fileSource").GetValue();
		}
	}
	[HarmonyPatch(typeof(Game), "Start")]
	public static class Game_Start_Patch
	{
		[HarmonyPrefix]
		public static void Prefix(Game __instance)
		{
			if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer())
			{
				ClientProfilePatches.ExpectServerProfile();
				NetworkManager.Instance.RegisterRPCs();
			}
			else if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
			{
				NetworkManager.Instance.RegisterRPCs();
			}
		}
	}
	[HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")]
	public static class PlayerProfile_SavePlayerToDisk_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(PlayerProfile __instance)
		{
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer())
			{
				return;
			}
			if (ClientProfilePatches.IsWaitingForProfile())
			{
				Plugin.Log.LogWarning((object)"[ClientProfilePatches] Suppressed profile upload to server (still waiting for authoritative server profile).");
				return;
			}
			Plugin.Log.LogInfo((object)"[ClientProfilePatches] Player saved — uploading profile to server.");
			try
			{
				byte[] data = ClientProfilePatches.ReadProfileBytes(__instance);
				NetworkManager.Instance.SendProfileDataToServer(data);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to upload saved profile to server: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(PlayerProfile), "LoadPlayerData")]
	public static class PlayerProfile_LoadPlayerData_Patch
	{
		[HarmonyPrefix]
		public static bool Prefix()
		{
			if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer() && ClientProfilePatches.IsWaitingForProfile())
			{
				Plugin.Log.LogInfo((object)"[ClientProfilePatches] Blocked local profile data while waiting for authoritative server profile.");
				return false;
			}
			return true;
		}

		[HarmonyPostfix]
		public static void Postfix(PlayerProfile __instance, Player player)
		{
			if (!ClientProfilePatches.FirstJoinInitializationPending)
			{
				return;
			}
			ClientProfilePatches.FirstJoinInitializationPending = false;
			ClientProfilePatches.IsFirstJoinInitializationActive = true;
			try
			{
				((Humanoid)player).UnequipAllItems();
				((Humanoid)player).GetInventory().RemoveAll();
				((Humanoid)player).GiveDefaultItems();
				player.SetGuardianPower(string.Empty);
				Traverse.Create((object)player).Field("m_skills").GetValue<Skills>()
					.Clear();
				player.m_customData.Clear();
				ClearPlayerCollection(player, "m_foods");
				ClearPlayerCollection(player, "m_knownRecipes");
				ClearPlayerCollection(player, "m_knownStations");
				ClearPlayerCollection(player, "m_knownMaterial");
				ClearPlayerCollection(player, "m_shownTutorials");
				ClearPlayerCollection(player, "m_uniques");
				ClearPlayerCollection(player, "m_trophies");
				ClearPlayerCollection(player, "m_knownBiome");
				ClearPlayerCollection(player, "m_knownTexts");
				__instance.SavePlayerData(player);
				byte[] data = (byte[])Traverse.Create((object)__instance).Field("m_playerData").GetValue();
				NetworkManager.Instance.SendProfileDataToServer(data, isPlayerData: true);
				Plugin.Log.LogInfo((object)"[ClientProfilePatches] Created initial clean player snapshot while preserving local appearance.");
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to initialize first-join player state: {arg}");
			}
			finally
			{
				ClientProfilePatches.IsFirstJoinInitializationActive = false;
			}
		}

		private static void ClearPlayerCollection(Player player, string fieldName)
		{
			Traverse.Create((object)player).Field(fieldName).Method("Clear", Array.Empty<object>())
				.GetValue();
		}
	}
	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	public static class ZNet_Disconnect_SaveProfile_Patch
	{
		[HarmonyPrefix]
		public static void Prefix()
		{
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || ClientProfilePatches.IsWaitingForProfile() || (Object)(object)Game.instance == (Object)null || Game.instance.GetPlayerProfile() == null)
			{
				return;
			}
			try
			{
				Plugin.Log.LogInfo((object)"[ClientProfilePatches] Saving profile before server disconnect.");
				ClientSyncManager.Instance?.FlushImmediate("disconnect flush");
				ClientProfilePatches.SafeSavePlayerProfile(setLogoutPoint: true);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to save profile before disconnect: {arg}");
			}
		}
	}
	[HarmonyPatch]
	public static class Inventory_Change_Checkpoint_Patch
	{
		public static IEnumerable<MethodBase> TargetMethods()
		{
			foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(Inventory)))
			{
				if (declaredMethod.Name == "Changed")
				{
					yield return declaredMethod;
				}
			}
		}

		[HarmonyPostfix]
		public static void Postfix(Inventory __instance)
		{
			if (LocalPlayerState.IsInventory(__instance))
			{
				ClientSyncManager.Instance.QueueSnapshotUpdate("inventory change");
			}
		}
	}
	[HarmonyPatch]
	public static class Skills_Change_Checkpoint_Patch
	{
		public static IEnumerable<MethodBase> TargetMethods()
		{
			foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(Skills)))
			{
				if (declaredMethod.Name == "LowerAllSkills" || declaredMethod.Name == "OnDeath")
				{
					yield return declaredMethod;
				}
			}
		}

		[HarmonyPostfix]
		public static void Postfix(Skills __instance)
		{
			if (LocalPlayerState.IsSkills(__instance))
			{
				ClientSyncManager.Instance.QueueSnapshotUpdate("skill change");
			}
		}
	}
	[HarmonyPatch(typeof(Player), "OnSkillLevelup")]
	public static class Player_SkillLevelup_Checkpoint_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(Player __instance)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer))
			{
				ClientSyncManager.Instance.QueueSnapshotUpdate("skill level up");
			}
		}
	}
	[HarmonyPatch]
	public static class Player_GuardianPower_Checkpoint_Patch
	{
		public static IEnumerable<MethodBase> TargetMethods()
		{
			foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(Player)))
			{
				if (declaredMethod.Name == "SetGuardianPower" || declaredMethod.Name == "SetForsakenPower")
				{
					yield return declaredMethod;
				}
			}
		}

		[HarmonyPrefix]
		public static void Prefix(Player __instance, out string __state)
		{
			__state = __instance.GetGuardianPowerName();
		}

		[HarmonyPostfix]
		public static void Postfix(Player __instance, string __state)
		{
			if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !(__state == __instance.GetGuardianPowerName()))
			{
				ClientSyncManager.Instance.QueueSnapshotUpdate("forsaken power change");
			}
		}
	}
	internal static class LocalPlayerState
	{
		public static bool IsInventory(Inventory inventory)
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				return false;
			}
			return Traverse.Create((object)Player.m_localPlayer).Field("m_inventory").GetValue() == inventory;
		}

		public static bool IsSkills(Skills skills)
		{
			if ((Object)(object)Player.m_localPlayer == (Object)null)
			{
				return false;
			}
			return Traverse.Create((object)Player.m_localPlayer).Field("m_skills").GetValue() == skills;
		}
	}
	[HarmonyPatch(typeof(FejdStartup))]
	public static class MenuWarningPatch
	{
		private static GameObject? _warningBanner;

		private const string WarningText = "<color=#FFCC00><b>CharactersVault:</b> Joining a server with this mod will wipe your selected character's items & skills!\nCreate a new character, and use that character on the server.</color>";

		[HarmonyPatch("SetupGui")]
		[HarmonyPostfix]
		public static void SetupGui_Postfix(FejdStartup __instance)
		{
			EnsureWarningBanner(__instance);
		}

		[HarmonyPatch("ShowCharacterSelection")]
		[HarmonyPostfix]
		public static void ShowCharacterSelection_Postfix(FejdStartup __instance)
		{
			EnsureWarningBanner(__instance);
		}

		private static void EnsureWarningBanner(FejdStartup startup)
		{
			//IL_0134: Unknown result type (might be due to invalid IL or missing references)
			//IL_0149: Unknown result type (might be due to invalid IL or missing references)
			//IL_015e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Unknown result type (might be due to invalid IL or missing references)
			//IL_0188: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (!ModConfig.ShowCharacterSelectWarning.Value)
				{
					if ((Object)(object)_warningBanner != (Object)null)
					{
						_warningBanner.SetActive(false);
					}
				}
				else if ((Object)(object)_warningBanner != (Object)null)
				{
					_warningBanner.SetActive(true);
				}
				else
				{
					if ((Object)(object)startup == (Object)null || (Object)(object)startup.m_selectCharacterPanel == (Object)null)
					{
						return;
					}
					Component val = Traverse.Create((object)startup).Field("m_csSourceInfo").GetValue<Component>() ?? Traverse.Create((object)startup).Field("m_csName").GetValue<Component>() ?? Traverse.Create((object)startup).Field("m_versionLabel").GetValue<Component>();
					if ((Object)(object)val == (Object)null)
					{
						ManualLogSource log = Plugin.Log;
						if (log != null)
						{
							log.LogWarning((object)"[MenuWarningPatch] Could not find reference text component to create character select warning banner.");
						}
						return;
					}
					Transform val2 = (((Object)(object)startup.m_characterSelectScreen != (Object)null) ? startup.m_characterSelectScreen.transform : startup.m_selectCharacterPanel.transform);
					_warningBanner = Object.Instantiate<GameObject>(val.gameObject, val2, false);
					((Object)_warningBanner).name = "CharactersVault_SelectWarningBanner";
					RectTransform component = _warningBanner.GetComponent<RectTransform>();
					if ((Object)(object)component != (Object)null)
					{
						component.anchorMin = new Vector2(0.5f, 1f);
						component.anchorMax = new Vector2(0.5f, 1f);
						component.pivot = new Vector2(0.5f, 1f);
						component.anchoredPosition = new Vector2(0f, -40f);
						component.sizeDelta = new Vector2(950f, 60f);
					}
					Component val3 = _warningBanner.GetComponent("TMP_Text") ?? _warningBanner.GetComponent("TextMeshProUGUI");
					if ((Object)(object)val3 != (Object)null)
					{
						Traverse obj = Traverse.Create((object)val3);
						obj.Property("text", (object[])null).SetValue((object)"<color=#FFCC00><b>CharactersVault:</b> Joining a server with this mod will wipe your selected character's items & skills!\nCreate a new character, and use that character on the server.</color>");
						obj.Property("fontSize", (object[])null).SetValue((object)15f);
						obj.Property("alignment", (object[])null).SetValue((object)2);
						obj.Property("enableWordWrapping", (object[])null).SetValue((object)true);
						obj.Property("richText", (object[])null).SetValue((object)true);
					}
					_warningBanner.SetActive(true);
					ManualLogSource log2 = Plugin.Log;
					if (log2 != null)
					{
						log2.LogInfo((object)"[MenuWarningPatch] Character select warning banner created successfully.");
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource log3 = Plugin.Log;
				if (log3 != null)
				{
					log3.LogWarning((object)("[MenuWarningPatch] Failed to create character select warning banner: " + ex.Message));
				}
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
	public static class ZNet_RPC_PeerInfo_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(ZNet __instance, ZRpc rpc)
		{
			try
			{
				if (!__instance.IsServer())
				{
					return;
				}
				ZNetPeer peerByRpc = ZNetHelper.GetPeerByRpc(rpc);
				if (peerByRpc == null)
				{
					return;
				}
				string playerId = ZNetHelper.GetPlayerId(peerByRpc);
				string playerName = peerByRpc.m_playerName;
				if (!ZNetHelper.IsValidPlayerId(playerId) || string.IsNullOrWhiteSpace(playerName))
				{
					if (ModConfig.VerboseLogging.Value)
					{
						Plugin.Log.LogInfo((object)"[ZNetPatch] Rejecting peer with invalid platform ID or empty name.");
					}
					KickPeer(peerByRpc, "Invalid player identity.");
					return;
				}
				Plugin.Log.LogInfo((object)("[CharacterVault] Player joining: platformId=" + playerId + ", Character='" + playerName + "'"));
				if (ModConfig.EnforceCharacterBinding.Value)
				{
					if (BindingManager.IsRegistered(playerId))
					{
						string registeredName = BindingManager.GetRegisteredName(playerId);
						if (!string.Equals(registeredName, playerName, StringComparison.OrdinalIgnoreCase))
						{
							Plugin.Log.LogWarning((object)("[CharacterVault] KICK " + playerId + ": tried '" + playerName + "', registered as '" + registeredName + "'"));
							KickPeer(peerByRpc, ModConfig.KickMessageWrongCharacter.Value);
							return;
						}
					}
					else
					{
						BindingManager.Register(playerId, playerName);
					}
				}
				BindingManager.RecordJoin(playerId);
				NetworkManager.Instance.SendHandshakeRequest(peerByRpc);
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ZNetPatch] Exception in RPC_PeerInfo patch: {arg}");
			}
		}

		private static void KickPeer(ZNetPeer peer, string reason)
		{
			try
			{
				Plugin.Log.LogWarning((object)$"[CharacterVault] Kicking peer {peer.m_uid}: {reason}");
				NetworkManager.Instance.RejectPeer(peer, reason);
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ZNetPatch] Error sending kick RPC: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Disconnect")]
	public static class ZNet_Disconnect_Patch
	{
		[HarmonyPrefix]
		public static void Prefix(ZNetPeer peer)
		{
			try
			{
				if (peer != null)
				{
					NetworkManager.Instance?.OnPeerDisconnected(peer.m_uid);
				}
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[ZNetPatch] Exception in Disconnect patch: {arg}");
			}
		}
	}
	[HarmonyPatch(typeof(FejdStartup), "ShowConnectError")]
	public static class FejdStartup_ShowConnectError_Patch
	{
		[HarmonyPostfix]
		public static void Postfix(FejdStartup __instance)
		{
			try
			{
				string text = ConnectionRejectionManager.ConsumeReason();
				if (!string.IsNullOrWhiteSpace(text))
				{
					Traverse.Create((object)__instance).Field("m_connectionFailedError").Property("text", (object[])null)
						.SetValue((object)text);
					Plugin.Log.LogInfo((object)("[ShowConnectError] Displaying rejection reason: " + text));
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ShowConnectError] Failed to set error text: " + ex.Message));
			}
		}
	}
}
namespace CharacterVault.Models
{
	public class CharacterRecord
	{
		public string PlayerId { get; set; } = string.Empty;

		public string CharacterName { get; set; } = string.Empty;

		public DateTime RegisteredAt { get; set; } = DateTime.UtcNow;

		public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
	}
	public class PlayerSnapshot
	{
		public string PlayerId { get; set; } = string.Empty;

		public string CharacterName { get; set; } = string.Empty;

		public DateTime SnapshotTime { get; set; } = DateTime.UtcNow;

		public string ProfileDataBase64 { get; set; } = string.Empty;

		public bool IsPlayerData { get; set; }

		public bool HasData => !string.IsNullOrEmpty(ProfileDataBase64);

		public byte[] GetProfileBytes()
		{
			if (!string.IsNullOrEmpty(ProfileDataBase64))
			{
				return Convert.FromBase64String(ProfileDataBase64);
			}
			return Array.Empty<byte>();
		}
	}
}
namespace CharacterVault.Helpers
{
	public static class ProfileHelper
	{
		private static bool _initialized;

		private static MethodInfo? _savePlayerToDiskMethod;

		private static FieldInfo? _playerNameField;

		private static FieldInfo? _filenameField;

		private static FieldInfo? _fileSourceField;

		private static void EnsureInitialized()
		{
			if (_initialized)
			{
				return;
			}
			_initialized = true;
			try
			{
				Type? typeFromHandle = typeof(PlayerProfile);
				_savePlayerToDiskMethod = typeFromHandle.GetMethod("SavePlayerToDisk", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				_playerNameField = typeFromHandle.GetField("m_playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				_filenameField = typeFromHandle.GetField("m_filename", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				_fileSourceField = typeFromHandle.GetField("m_fileSource", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (_savePlayerToDiskMethod == null)
				{
					Plugin.Log.LogWarning((object)"[CharacterVault :: ProfileHelper] Could not find PlayerProfile.SavePlayerToDisk method via reflection.");
				}
				if (_playerNameField == null || _filenameField == null)
				{
					Plugin.Log.LogWarning((object)"[CharacterVault :: ProfileHelper] Could not find name/filename fields on PlayerProfile.");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] Reflection init failed: " + ex.Message));
			}
		}

		public static byte[] CreateBlankProfile(string characterName)
		{
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Expected O, but got Unknown
			Plugin.Log.LogInfo((object)("[CharacterVault :: ProfileHelper] Generating native blank .fch profile for character '" + characterName + "'..."));
			EnsureInitialized();
			if (_savePlayerToDiskMethod == null)
			{
				Plugin.Log.LogError((object)"[CharacterVault :: ProfileHelper] Reflection targets missing — cannot generate blank profile.");
				return Array.Empty<byte>();
			}
			try
			{
				Type typeFromHandle = typeof(PlayerProfile);
				PlayerProfile val = null;
				if (_fileSourceField != null)
				{
					try
					{
						object obj = Enum.Parse(_fileSourceField.FieldType, "Local");
						val = (PlayerProfile)Activator.CreateInstance(typeFromHandle, characterName, obj);
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("[CharacterVault :: ProfileHelper] Activator.CreateInstance failed: " + ex.Message));
					}
				}
				if (val == null)
				{
					Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] Failed to instantiate PlayerProfile for '" + characterName + "'."));
					return Array.Empty<byte>();
				}
				val.SetName(characterName);
				_savePlayerToDiskMethod.Invoke(val, null);
				string path = val.GetPath();
				if (File.Exists(path))
				{
					byte[] array = File.ReadAllBytes(path);
					try
					{
						File.Delete(path);
					}
					catch
					{
					}
					Plugin.Log.LogInfo((object)$"[CharacterVault :: ProfileHelper] SUCCESS: Generated {array.Length}-byte native blank profile for '{characterName}'.");
					return array;
				}
				Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] SavePlayerToDisk did not produce expected file at '" + path + "'."));
				return Array.Empty<byte>();
			}
			catch (Exception ex2)
			{
				if (ex2 is TargetInvocationException { InnerException: not null } ex3)
				{
					Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] Failed to generate blank profile for '" + characterName + "': " + ex3.InnerException.GetType().Name + " - " + ex3.InnerException.Message + "\nStackTrace:\n" + ex3.InnerException.StackTrace));
				}
				else
				{
					Plugin.Log.LogError((object)$"[CharacterVault :: ProfileHelper] Failed to generate blank profile for '{characterName}': {ex2}");
				}
				return Array.Empty<byte>();
			}
		}
	}
	public static class SimpleJson
	{
		public static string SerializeObject(object? obj, bool prettyPrint = true)
		{
			StringBuilder stringBuilder = new StringBuilder();
			SerializeValue(obj, stringBuilder, (!prettyPrint) ? (-1) : 0);
			return stringBuilder.ToString();
		}

		public static T? DeserializeObject<T>(string json) where T : new()
		{
			if (string.IsNullOrWhiteSpace(json))
			{
				return default(T);
			}
			object obj = Deserialize(json);
			if (obj == null)
			{
				return default(T);
			}
			if (obj is T)
			{
				return (T)obj;
			}
			if (typeof(IDictionary).IsAssignableFrom(typeof(T)) && obj is IDictionary<string, object> dictionary)
			{
				Type[] genericArguments = typeof(T).GetGenericArguments();
				if (genericArguments.Length == 2 && genericArguments[0] == typeof(string))
				{
					Type targetType = genericArguments[1];
					IDictionary dictionary2 = (IDictionary)Activator.CreateInstance(typeof(T));
					foreach (KeyValuePair<string, object> item in dictionary)
					{
						if (item.Value is IDictionary<string, object> dict)
						{
							object value = ConvertDictionaryToObject(dict, targetType);
							dictionary2.Add(item.Key, value);
						}
						else
						{
							dictionary2.Add(item.Key, ConvertValue(item.Value, targetType));
						}
					}
					return (T)dictionary2;
				}
			}
			if (obj is IDictionary<string, object> dict2)
			{
				return (T)ConvertDictionaryToObject(dict2, typeof(T));
			}
			return default(T);
		}

		private static object ConvertDictionaryToObject(IDictionary<string, object?> dict, Type targetType)
		{
			object obj = Activator.CreateInstance(targetType);
			PropertyInfo[] properties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
			FieldInfo[] fields = targetType.GetFields(BindingFlags.Instance | BindingFlags.Public);
			PropertyInfo[] array = properties;
			foreach (PropertyInfo propertyInfo in array)
			{
				if (propertyInfo.CanWrite && FindKey(dict, propertyInfo.Name, out object value))
				{
					propertyInfo.SetValue(obj, ConvertValue(value, propertyInfo.PropertyType), null);
				}
			}
			FieldInfo[] array2 = fields;
			foreach (FieldInfo fieldInfo in array2)
			{
				if (FindKey(dict, fieldInfo.Name, out object value2))
				{
					fieldInfo.SetValue(obj, ConvertValue(value2, fieldInfo.FieldType));
				}
			}
			return obj;
		}

		private static bool FindKey(IDictionary<string, object?> dict, string name, out object? value)
		{
			if (dict.TryGetValue(name, out value))
			{
				return true;
			}
			foreach (KeyValuePair<string, object> item in dict)
			{
				if (string.Equals(item.Key, name, StringComparison.OrdinalIgnoreCase))
				{
					value = item.Value;
					return true;
				}
			}
			value = null;
			return false;
		}

		private static object? ConvertValue(object? val, Type targetType)
		{
			if (val == null)
			{
				return null;
			}
			if (targetType.IsAssignableFrom(val.GetType()))
			{
				return val;
			}
			if (targetType == typeof(DateTime))
			{
				if (val is string s && DateTime.TryParse(s, null, DateTimeStyles.RoundtripKind, out var result))
				{
					return result;
				}
				return default(DateTime);
			}
			if (targetType == typeof(bool))
			{
				if (val is bool flag)
				{
					return flag;
				}
				if (val is string value && bool.TryParse(value, out var result2))
				{
					return result2;
				}
				return false;
			}
			if (targetType == typeof(int))
			{
				return Convert.ToInt32(val, CultureInfo.InvariantCulture);
			}
			if (targetType == typeof(long))
			{
				return Convert.ToInt64(val, CultureInfo.InvariantCulture);
			}
			if (targetType == typeof(float))
			{
				return Convert.ToSingle(val, CultureInfo.InvariantCulture);
			}
			if (targetType == typeof(double))
			{
				return Convert.ToDouble(val, CultureInfo.InvariantCulture);
			}
			if (targetType == typeof(string))
			{
				return val.ToString();
			}
			return val;
		}

		private static void SerializeValue(object? value, StringBuilder sb, int indentLevel)
		{
			if (value == null)
			{
				sb.Append("null");
			}
			else if (value is string str)
			{
				sb.Append('"').Append(EscapeString(str)).Append('"');
			}
			else if (value is bool flag)
			{
				sb.Append(flag ? "true" : "false");
			}
			else if (value is DateTime dateTime)
			{
				sb.Append('"').Append(dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)).Append('"');
			}
			else if (value is int || value is long || value is short || value is byte)
			{
				sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture));
			}
			else if (value is float num)
			{
				sb.Append(num.ToString("R", CultureInfo.InvariantCulture));
			}
			else if (value is double num2)
			{
				sb.Append(num2.ToString("R", CultureInfo.InvariantCulture));
			}
			else if (value is IDictionary dict)
			{
				SerializeDictionary(dict, sb, indentLevel);
			}
			else if (value is IEnumerable list)
			{
				SerializeList(list, sb, indentLevel);
			}
			else
			{
				SerializeObjectFields(value, sb, indentLevel);
			}
		}

		private static void SerializeDictionary(IDictionary dict, StringBuilder sb, int indentLevel)
		{
			bool flag = indentLevel >= 0;
			if (dict.Count == 0)
			{
				sb.Append("{}");
				return;
			}
			sb.Append('{');
			if (flag)
			{
				sb.Append('\n');
			}
			int num = 0;
			foreach (DictionaryEntry item in dict)
			{
				if (flag)
				{
					sb.Append(' ', (indentLevel + 1) * 2);
				}
				sb.Append('"').Append(EscapeString(item.Key?.ToString() ?? string.Empty)).Append("\":");
				if (flag)
				{
					sb.Append(' ');
				}
				SerializeValue(item.Value, sb, flag ? (indentLevel + 1) : (-1));
				if (++num < dict.Count)
				{
					sb.Append(',');
				}
				if (flag)
				{
					sb.Append('\n');
				}
			}
			if (flag)
			{
				sb.Append(' ', indentLevel * 2);
			}
			sb.Append('}');
		}

		private static void SerializeList(IEnumerable list, StringBuilder sb, int indentLevel)
		{
			bool flag = indentLevel >= 0;
			sb.Append('[');
			bool flag2 = true;
			foreach (object item in list)
			{
				if (!flag2)
				{
					sb.Append(flag ? ", " : ",");
				}
				SerializeValue(item, sb, flag ? (indentLevel + 1) : (-1));
				flag2 = false;
			}
			sb.Append(']');
		}

		private static void SerializeObjectFields(object obj, StringBuilder sb, int indentLevel)
		{
			bool flag = indentLevel >= 0;
			Type type = obj.GetType();
			PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
			FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
			List<(string, object)> list = new List<(string, object)>();
			PropertyInfo[] array = properties;
			foreach (PropertyInfo propertyInfo in array)
			{
				if (propertyInfo.CanRead && propertyInfo.CanWrite)
				{
					list.Add((propertyInfo.Name, propertyInfo.GetValue(obj, null)));
				}
			}
			FieldInfo[] array2 = fields;
			foreach (FieldInfo fieldInfo in array2)
			{
				list.Add((fieldInfo.Name, fieldInfo.GetValue(obj)));
			}
			if (list.Count == 0)
			{
				sb.Append("{}");
				return;
			}
			sb.Append('{');
			if (flag)
			{
				sb.Append('\n');
			}
			for (int j = 0; j < list.Count; j++)
			{
				if (flag)
				{
					sb.Append(' ', (indentLevel + 1) * 2);
				}
				sb.Append('"').Append(EscapeString(list[j].Item1)).Append("\":");
				if (flag)
				{
					sb.Append(' ');
				}
				SerializeValue(list[j].Item2, sb, flag ? (indentLevel + 1) : (-1));
				if (j < list.Count - 1)
				{
					sb.Append(',');
				}
				if (flag)
				{
					sb.Append('\n');
				}
			}
			if (flag)
			{
				sb.Append(' ', indentLevel * 2);
			}
			sb.Append('}');
		}

		private static string EscapeString(string str)
		{
			StringBuilder stringBuilder = new StringBuilder(str.Length + 4);
			foreach (char c in str)
			{
				switch (c)
				{
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '\b':
					stringBuilder.Append("\\b");
					continue;
				case '\f':
					stringBuilder.Append("\\f");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\r':
					stringBuilder.Append("\\r");
					continue;
				case '\t':
					stringBuilder.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}

		public static object? Deserialize(string json)
		{
			int index = 0;
			return ParseValue(json, ref index);
		}

		private static object? ParseValue(string json, ref int index)
		{
			SkipWhitespace(json, ref index);
			if (index >= json.Length)
			{
				return null;
			}
			char c = json[index];
			switch (c)
			{
			case '{':
				return ParseObject(json, ref index);
			case '[':
				return ParseArray(json, ref index);
			case '"':
				return ParseString(json, ref index);
			case 'f':
			case 't':
				return ParseBool(json, ref index);
			case 'n':
				return ParseNull(json, ref index);
			default:
				if (char.IsDigit(c) || c == '-')
				{
					return ParseNumber(json, ref index);
				}
				return null;
			}
		}

		private static Dictionary<string, object?> ParseObject(string json, ref int index)
		{
			Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
			index++;
			while (index < json.Length)
			{
				SkipWhitespace(json, ref index);
				if (index >= json.Length)
				{
					break;
				}
				if (json[index] == '}')
				{
					index++;
					break;
				}
				string key = ParseString(json, ref index);
				SkipWhitespace(json, ref index);
				if (index < json.Length && json[index] == ':')
				{
					index++;
				}
				object value = ParseValue(json, ref index);
				dictionary[key] = value;
				SkipWhitespace(json, ref index);
				if (index < json.Length && json[index] == ',')
				{
					index++;
				}
			}
			return dictionary;
		}

		private static List<object?> ParseArray(string json, ref int index)
		{
			List<object> list = new List<object>();
			index++;
			while (index < json.Length)
			{
				SkipWhitespace(json, ref index);
				if (index >= json.Length)
				{
					break;
				}
				if (json[index] == ']')
				{
					index++;
					break;
				}
				list.Add(ParseValue(json, ref index));
				SkipWhitespace(json, ref index);
				if (index < json.Length && json[index] == ',')
				{
					index++;
				}
			}
			return list;
		}

		private static string ParseString(string json, ref int index)
		{
			StringBuilder stringBuilder = new StringBuilder();
			index++;
			while (index < json.Length)
			{
				char c = json[index++];
				if (c == '"')
				{
					break;
				}
				if (c == '\\' && index < json.Length)
				{
					char c2 = json[index++];
					switch (c2)
					{
					case '"':
						stringBuilder.Append('"');
						break;
					case '\\':
						stringBuilder.Append('\\');
						break;
					case '/':
						stringBuilder.Append('/');
						break;
					case 'b':
						stringBuilder.Append('\b');
						break;
					case 'f':
						stringBuilder.Append('\f');
						break;
					case 'n':
						stringBuilder.Append('\n');
						break;
					case 'r':
						stringBuilder.Append('\r');
						break;
					case 't':
						stringBuilder.Append('\t');
						break;
					case 'u':
					{
						if (index + 4 <= json.Length && int.TryParse(json.Substring(index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result))
						{
							stringBuilder.Append((char)result);
							index += 4;
						}
						break;
					}
					default:
						stringBuilder.Append(c2);
						break;
					}
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.ToString();
		}

		private static bool ParseBool(string json, ref int index)
		{
			if (json.Substring(index).StartsWith("true", StringComparison.OrdinalIgnoreCase))
			{
				index += 4;
				return true;
			}
			if (json.Substring(index).StartsWith("false", StringComparison.OrdinalIgnoreCase))
			{
				index += 5;
				return false;
			}
			index++;
			return false;
		}

		private static object? ParseNull(string json, ref int index)
		{
			if (json.Substring(index).StartsWith("null", StringComparison.OrdinalIgnoreCase))
			{
				index += 4;
			}
			return null;
		}

		private static object ParseNumber(string json, ref int index)
		{
			int num = index;
			while (index < json.Length && (char.IsDigit(json[index]) || json[index] == '-' || json[index] == '+' || json[index] == '.' || json[index] == 'e' || json[index] == 'E'))
			{
				index++;
			}
			string text = json.Substring(num, index - num);
			long result2;
			if (text.Contains(".") || text.Contains("e") || text.Contains("E"))
			{
				if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
				{
					return result;
				}
			}
			else if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out result2))
			{
				if (result2 <= int.MaxValue && result2 >= int.MinValue)
				{
					return (int)result2;
				}
				return result2;
			}
			return text;
		}

		private static void SkipWhitespace(string json, ref int index)
		{
			while (index < json.Length && char.IsWhiteSpace(json[index]))
			{
				index++;
			}
		}
	}
	internal static class ZNetHelper
	{
		private static readonly FieldInfo? FiPeers = typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

		private static readonly FieldInfo? FiAdminList = typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

		private static readonly MethodInfo? MiListContainsId = typeof(ZNet).GetMethod("ListContainsId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

		public static List<ZNetPeer> GetPeers()
		{
			if ((Object)(object)ZNet.instance == (Object)null || FiPeers == null)
			{
				return new List<ZNetPeer>();
			}
			if (!(FiPeers.GetValue(ZNet.instance) is List<ZNetPeer> collection))
			{
				return new List<ZNetPeer>();
			}
			return new List<ZNetPeer>(collection);
		}

		public static ZNetPeer? GetPeerByRpc(ZRpc rpc)
		{
			return ((IEnumerable<ZNetPeer>)GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_rpc == rpc));
		}

		public static ZNetPeer? FindPeerByPlayerId(string playerId)
		{
			if (string.IsNullOrWhiteSpace(playerId))
			{
				return null;
			}
			return ((IEnumerable<ZNetPeer>)GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p != null && string.Equals(GetPlayerId(p), playerId, StringComparison.OrdinalIgnoreCase)));
		}

		public static string GetPlayerId(ZNetPeer peer)
		{
			if (peer == null || peer.m_socket == null)
			{
				return string.Empty;
			}
			string hostName = peer.m_socket.GetHostName();
			if (string.IsNullOrWhiteSpace(hostName))
			{
				return string.Empty;
			}
			if (ulong.TryParse(hostName, out var _))
			{
				return "Steam_" + hostName;
			}
			return hostName;
		}

		public static bool IsValidPlayerId(string? playerId)
		{
			if (string.IsNullOrWhiteSpace(playerId) || playerId.Length > 128)
			{
				return false;
			}
			foreach (char c in playerId)
			{
				if (!char.IsLetterOrDigit(c) && c != '_' && c != '-')
				{
					return false;
				}
			}
			ulong result;
			if (!playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase) && !playerId.StartsWith("Xbox_", StringComparison.OrdinalIgnoreCase) && !playerId.StartsWith("PlayFab_", StringComparison.OrdinalIgnoreCase))
			{
				return ulong.TryParse(playerId, out result);
			}
			return true;
		}

		public static bool IsAdmin(ZNetPeer peer)
		{
			if (peer == null)
			{
				return false;
			}
			string playerId = GetPlayerId(peer);
			ISocket socket = peer.m_socket;
			string text = ((socket != null) ? socket.GetHostName() : null) ?? string.Empty;
			if (!IsAdmin(playerId))
			{
				if (!string.IsNullOrWhiteSpace(text))
				{
					return IsAdmin(text);
				}
				return false;
			}
			return true;
		}

		public static bool IsAdmin(string playerId)
		{
			if ((Object)(object)ZNet.instance == (Object)null || string.IsNullOrWhiteSpace(playerId))
			{
				return false;
			}
			try
			{
				object obj = ((FiAdminList != null) ? FiAdminList.GetValue(ZNet.instance) : Traverse.Create((object)ZNet.instance).Field("m_adminList").GetValue());
				if (obj == null)
				{
					return false;
				}
				HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { playerId.Trim() };
				ulong result;
				if (playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase))
				{
					hashSet.Add(playerId.Substring("Steam_".Length).Trim());
				}
				else if (ulong.TryParse(playerId.Trim(), out result))
				{
					hashSet.Add("Steam_" + playerId.Trim());
				}
				if (playerId.StartsWith("Xbox_", StringComparison.OrdinalIgnoreCase))
				{
					hashSet.Add(playerId.Substring("Xbox_".Length).Trim());
				}
				if (playerId.StartsWith("PlayFab_", StringComparison.OrdinalIgnoreCase))
				{
					hashSet.Add(playerId.Substring("PlayFab_".Length).Trim());
				}
				if (MiListContainsId != null)
				{
					foreach (string item in hashSet)
					{
						if ((bool)(MiListContainsId.Invoke(ZNet.instance, new object[2] { obj, item }) ?? ((object)false)))
						{
							return true;
						}
					}
				}
				MethodInfo method = obj.GetType().GetMethod("Contains", new Type[1] { typeof(string) });
				if (method != null)
				{
					foreach (string item2 in hashSet)
					{
						if ((bool)(method.Invoke(obj, new object[1] { item2 }) ?? ((object)false)))
						{
							return true;
						}
					}
				}
				MethodInfo method2 = obj.GetType().GetMethod("GetList");
				if (method2 != null && method2.Invoke(obj, null) is IEnumerable enumerable)
				{
					foreach (object item3 in enumerable)
					{
						if (item3 is string text && hashSet.Contains(text.Trim()))
						{
							return true;
						}
					}
				}
				return false;
			}
			catch (Exception ex)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogError((object)("[ZNetHelper] IsAdmin check failed: " + ex.Message));
				}
				return false;
			}
		}
	}
}