Decompiled source of ServerCharacterVault v1.0.0

BepInEx\plugins\ServerCharacterVault\ServerCharacterVault.dll

Decompiled 13 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using ServerCharacterVault.Helpers;
using ServerCharacterVault.Models;
using ServerCharacterVault.Patches;
using ServerCharacterVault.Systems;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[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 ServerCharacterVault
{
	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> 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");
			VerboseLogging = cfg.Bind<bool>("Debug", "VerboseLogging", false, "Enable extra debug logging to the BepInEx console/log file.");
		}
	}
	[BepInPlugin("com.servercharactervault.valheim", "ServerCharacterVault", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		public const string ModGuid = "com.servercharactervault.valheim";

		public const string ModName = "ServerCharacterVault";

		public const string ModVersion = "1.0.0";

		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)"  ServerCharacterVault v1.0.0 loading...");
				Log.LogInfo((object)"══════════════════════════════════════════");
				ModConfig.Initialize(((BaseUnityPlugin)this).Config);
				DataStore.Initialize();
				BindingManager.Load();
				_harmony = new Harmony("com.servercharactervault.valheim");
				_harmony.PatchAll();
				Log.LogInfo((object)"[ServerCharacterVault] All Harmony patches applied.");
				Log.LogInfo((object)"[ServerCharacterVault] Loaded successfully. Waiting for network initialization...");
				NetworkManager.Initialize();
				ClientSyncManager.Initialize();
			}
			catch (Exception arg)
			{
				Log.LogError((object)string.Format("[{0}] FATAL: Failed to initialize — {1}", "ServerCharacterVault", arg));
			}
		}

		private void Start()
		{
		}

		private void OnDestroy()
		{
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			Log.LogInfo((object)"[ServerCharacterVault] Unloaded.");
		}
	}
}
namespace ServerCharacterVault.Systems
{
	public static class AdminCommandHandler
	{
		private static readonly string[] Prefixes = new string[2] { "/scv", "/sc" };

		public static bool IsCommand(string text)
		{
			if (string.IsNullOrWhiteSpace(text))
			{
				return false;
			}
			string[] prefixes = Prefixes;
			foreach (string text2 in prefixes)
			{
				if (text.StartsWith(text2, StringComparison.OrdinalIgnoreCase) && (text.Length == text2.Length || text[text2.Length] == ' '))
				{
					return true;
				}
			}
			return false;
		}

		private static string GetMatchingPrefix(string text)
		{
			string[] prefixes = Prefixes;
			foreach (string text2 in prefixes)
			{
				if (text.StartsWith(text2, StringComparison.OrdinalIgnoreCase))
				{
					return text2;
				}
			}
			return "/scv";
		}

		public static bool Handle(long senderUid, string text)
		{
			ZNet instance = ZNet.instance;
			ZNetPeer val = ((instance != null) ? instance.GetPeer(senderUid) : null);
			if (val == null)
			{
				return false;
			}
			string playerId = ZNetHelper.GetPlayerId(val);
			if (!IsAdmin(playerId))
			{
				Plugin.Log.LogWarning((object)("[AdminCmd] Non-admin " + playerId + " tried to run: " + text));
				return false;
			}
			string matchingPrefix = GetMatchingPrefix(text);
			string[] array = text.Substring(matchingPrefix.Length).Trim().Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			if (array.Length == 0)
			{
				LogToAdmin(val, PrintHelp());
				return true;
			}
			string text2 = array[0].ToLowerInvariant();
			switch (text2)
			{
			case "remove":
				return CmdRemove(val, array);
			case "wipe":
				return CmdWipe(val, array);
			case "list":
				return CmdList(val);
			case "status":
				return CmdStatus(val, array);
			case "help":
				LogToAdmin(val, PrintHelp());
				return true;
			default:
				LogToAdmin(val, "Unknown command '" + text2 + "'. Type /scv help for a list.");
				return true;
			}
		}

		private static bool CmdRemove(ZNetPeer adminPeer, string[] tokens)
		{
			if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId))
			{
				return true;
			}
			bool flag = BindingManager.RemoveBinding(targetId);
			LogToAdmin(adminPeer, flag ? ("Removed binding for " + targetId + ". They may re-register with a new character.") : ("No binding found for " + targetId + "."));
			return true;
		}

		private static bool CmdWipe(ZNetPeer adminPeer, string[] tokens)
		{
			if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId))
			{
				return true;
			}
			bool num = DataStore.WipePlayerData(targetId);
			BindingManager.Load();
			if (num)
			{
				LogToAdmin(adminPeer, "Wiped all server data for " + targetId + ". On their next join they will receive a blank character (no items, no skills).");
			}
			else
			{
				LogToAdmin(adminPeer, "No data found for " + targetId + " — nothing to wipe.");
			}
			return true;
		}

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

		private static bool CmdStatus(ZNetPeer adminPeer, string[] tokens)
		{
			if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId))
			{
				return true;
			}
			string registeredName = BindingManager.GetRegisteredName(targetId);
			PlayerSnapshot playerSnapshot = DataStore.LoadSnapshot(targetId);
			StringBuilder stringBuilder = new StringBuilder("Status for " + targetId + ":\n");
			stringBuilder.AppendLine("  Binding:   " + ((registeredName != null) ? ("'" + registeredName + "'") : "Not registered"));
			stringBuilder.Append("  Snapshot:  " + ((playerSnapshot != null) ? $"Taken {playerSnapshot.SnapshotTime:yyyy-MM-dd HH:mm} UTC" : "None"));
			LogToAdmin(adminPeer, stringBuilder.ToString());
			return true;
		}

		private static bool TryGetPlayerId(string[] tokens, int index, ZNetPeer adminPeer, out string targetId)
		{
			targetId = "";
			if (tokens.Length <= index)
			{
				LogToAdmin(adminPeer, "Invalid or missing player ID. Example: /scv " + tokens[0] + " Steam_76561198XXXXXXXXX");
				return false;
			}
			targetId = tokens[index];
			if (ZNetHelper.IsValidPlayerId(targetId))
			{
				return true;
			}
			LogToAdmin(adminPeer, "Invalid player ID. Use the platform ID shown in /scv list, such as Steam_... or Xbox_....");
			return false;
		}

		private static bool IsAdmin(string playerId)
		{
			if ((Object)(object)ZNet.instance != (Object)null)
			{
				return ZNetHelper.IsAdmin(playerId);
			}
			return false;
		}

		private static void LogToAdmin(ZNetPeer adminPeer, string message)
		{
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			Plugin.Log.LogInfo((object)("[AdminCmd] → " + message));
			try
			{
				if (adminPeer != null)
				{
					ZRoutedRpc.instance.InvokeRoutedRPC(adminPeer.m_uid, "ChatMessage", new object[4] { adminPeer.m_refPos, 1, "ServerCharacterVault", message });
				}
			}
			catch (Exception ex)
			{
				if (ModConfig.VerboseLogging.Value)
				{
					Plugin.Log.LogWarning((object)("[AdminCmd] Could not send in-game response: " + ex.Message));
				}
			}
		}

		private static string PrintHelp()
		{
			return "ServerCharacterVault Admin Commands:\n  /scv remove [playerId] — Remove character binding (allows re-register)\n  /scv wipe [playerId]   — Delete ALL server data for player (blank slate next join)\n  /scv list              — Show all bindings\n  /scv status [playerId] — Show binding + snapshot info\n  /scv help              — This message\n  (Note: /sc prefix is also supported)";
		}
	}
	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)
		{
			if (!_bindings.Remove(playerId))
			{
				return false;
			}
			Save();
			Plugin.Log.LogInfo((object)("[BindingManager] Removed binding for " + playerId));
			return true;
		}

		public static IReadOnlyDictionary<string, CharacterRecord> GetAll()
		{
			return new Dictionary<string, CharacterRecord>(_bindings);
		}
	}
	public class ClientSyncManager : MonoBehaviour
	{
		private Coroutine? _syncCoroutine;

		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("ServerCharacterVault_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);
			}
		}

		public void QueueSnapshotUpdate(string reason)
		{
			if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || ClientProfilePatches.IsWaitingForProfile() || ClientProfilePatches.IsInitializingFirstJoin())
			{
				return;
			}
			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.");
						Game.instance.SavePlayerProfile(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;

		private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
		{
			Formatting = (Formatting)1,
			NullValueHandling = (NullValueHandling)1
		};

		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, "ServerCharacterVault");
			_snapshotsDir = Path.Combine(_rootDir, "snapshots");
			Directory.CreateDirectory(_rootDir);
			Directory.CreateDirectory(_snapshotsDir);
			Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Root Data Directory: " + _rootDir));
			Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Snapshots Directory: " + _snapshotsDir));
		}

		public static Dictionary<string, CharacterRecord> LoadBindings()
		{
			try
			{
				if (!File.Exists(BindingsFilePath))
				{
					return new Dictionary<string, CharacterRecord>();
				}
				return JsonConvert.DeserializeObject<Dictionary<string, CharacterRecord>>(File.ReadAllText(BindingsFilePath), JsonSettings) ?? new Dictionary<string, CharacterRecord>();
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ServerCharacterVault :: 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 = JsonConvert.SerializeObject((object)bindings, JsonSettings);
				WriteAllTextAtomically(BindingsFilePath, content);
				Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: DataStore] Saved {bindings.Count} binding(s) to '{BindingsFilePath}'");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)("[ServerCharacterVault :: DataStore] No snapshot file found at '" + text + "'"));
					return null;
				}
				PlayerSnapshot result = JsonConvert.DeserializeObject<PlayerSnapshot>(File.ReadAllText(text), JsonSettings);
				Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Loaded snapshot file for platform ID " + playerId + " from '" + text + "'"));
				return result;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ServerCharacterVault :: 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 = JsonConvert.SerializeObject((object)snapshot, JsonSettings);
				WriteAllTextAtomically(text, content);
				Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Saved snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "') -> '" + text + "'"));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ServerCharacterVault :: DataStore] Failed to save snapshot for platform ID " + snapshot.PlayerId + ": " + ex.Message));
			}
		}

		public static bool WipePlayerData(string playerId)
		{
			bool result = false;
			string path = SnapshotPath(playerId);
			if (File.Exists(path))
			{
				try
				{
					File.Delete(path);
					Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Wipe: deleted snapshot file for platform ID " + playerId + "."));
					result = true;
				}
				catch (Exception ex)
				{
					Plugin.Log.LogError((object)("[ServerCharacterVault :: DataStore] WIPE FAILED: Could not delete snapshot for " + playerId + ": " + ex.Message));
				}
			}
			Dictionary<string, CharacterRecord> dictionary = LoadBindings();
			if (dictionary.ContainsKey(playerId))
			{
				dictionary.Remove(playerId);
				SaveBindings(dictionary);
				Plugin.Log.LogInfo((object)("[ServerCharacterVault :: 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 = "ServerCharacterVault_Handshake";

		private const string RpcSaveProfileChunk = "ServerCharacterVault_SaveProfileChunk";

		private const string RpcProfileDataChunk = "ServerCharacterVault_ProfileDataChunk";

		private const string RpcKickReason = "ServerCharacterVault_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("ServerCharacterVault_NetworkManager");
			Instance = val.AddComponent<NetworkManager>();
			Object.DontDestroyOnLoad((Object)val);
			Plugin.Log.LogInfo((object)"[ServerCharacterVault :: Network] NetworkManager initialized.");
		}

		public void RegisterRPCs()
		{
			ZRoutedRpc.instance.Register<string>("ServerCharacterVault_Handshake", (Action<long, string>)RPC_Handshake);
			ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("ServerCharacterVault_ProfileDataChunk", (Method<int, int, bool, bool, ZPackage>)RPC_ProfileDataChunk);
			ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("ServerCharacterVault_SaveProfileChunk", (Method<int, int, bool, bool, ZPackage>)RPC_SaveProfileChunk);
			ZRoutedRpc.instance.Register<string>("ServerCharacterVault_KickReason", (Action<long, string>)RPC_KickReason);
			Plugin.Log.LogInfo((object)"[ServerCharacterVault :: 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)("[ServerCharacterVault :: Network] Server rejection reason: " + reason));
			}
		}

		public void RejectPeer(ZNetPeer peer, string reason)
		{
			if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer())
			{
				ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "ServerCharacterVault_KickReason", new object[1] { reason });
				((MonoBehaviour)this).StartCoroutine(DisconnectRejectedPeer(peer));
			}
		}

		private IEnumerator DisconnectRejectedPeer(ZNetPeer peer)
		{
			yield return null;
			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 != "1.0.0")
				{
					Plugin.Log.LogWarning((object)string.Format("[ServerCharacterVault :: Network] Peer {0} wrong mod version: '{1}' (expected '{2}'). KICKING.", sender, version, "1.0.0"));
					ZNetPeer peer = ZNet.instance.GetPeer(sender);
					if (peer != null)
					{
						ZNet.instance.Disconnect(peer);
					}
					return;
				}
				Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Peer {sender} COMPLETED HANDSHAKE (v{version}).");
				_handshakeCompleted.Add(sender);
				ZNetPeer peer2 = ZNet.instance.GetPeer(sender);
				if (peer2 == null)
				{
					Plugin.Log.LogWarning((object)$"[ServerCharacterVault :: Network] Peer {sender} null after handshake!");
					return;
				}
				string playerId = ZNetHelper.GetPlayerId(peer2);
				Plugin.Log.LogInfo((object)("[ServerCharacterVault :: Network] Checking snapshot store for platform ID " + playerId + " ('" + peer2.m_playerName + "')..."));
				PlayerSnapshot snapshot = SnapshotManager.GetSnapshot(playerId);
				byte[] array;
				if (snapshot != null && snapshot.HasData)
				{
					array = snapshot.GetProfileBytes();
					Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> sending existing stored profile ({array.Length} bytes).");
				}
				else
				{
					Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> no stored snapshot (first join). Requesting client-side clean initialization.");
					array = Array.Empty<byte>();
				}
				SendProfileDataToClient(sender, array, snapshot?.IsPlayerData ?? false, snapshot == null);
			}
			else if (version == "request")
			{
				Plugin.Log.LogInfo((object)"[ServerCharacterVault :: Network] Server requested handshake. Replying with version '1.0.0'.");
				ZRoutedRpc.instance.InvokeRoutedRPC(sender, "ServerCharacterVault_Handshake", new object[1] { "1.0.0" });
			}
		}

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

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

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

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

		public void SendProfileDataToServer(byte[] data, bool isPlayerData = false)
		{
			Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Transmitting {data.Length} profile bytes to server...");
			SendChunks(0L, "ServerCharacterVault_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)$"[ServerCharacterVault :: 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)
			{
				ZNetPeer peer = ZNet.instance.GetPeer(sender);
				if (peer != null)
				{
					string playerId = ZNetHelper.GetPlayerId(peer);
					Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: Snapshot] Found existing snapshot for platform ID {playerId} ('{playerSnapshot.CharacterName}'), created: {playerSnapshot.SnapshotTime}");
			}
			else
			{
				Plugin.Log.LogInfo((object)("[ServerCharacterVault :: Snapshot] No existing snapshot found for platform ID " + playerId + "."));
			}
			return playerSnapshot;
		}

		public static void SaveSnapshot(PlayerSnapshot snapshot)
		{
			Plugin.Log.LogInfo((object)("[ServerCharacterVault :: Snapshot] Saving snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "')..."));
			DataStore.SaveSnapshot(snapshot);
		}
	}
}
namespace ServerCharacterVault.Patches
{
	[HarmonyPatch(typeof(Chat), "RPC_ChatMessage")]
	public static class Chat_RPC_ChatMessage_Patch
	{
		[HarmonyPrefix]
		public static bool Prefix(long sender, Vector3 position, int type, UserInfo userInfo, string text)
		{
			try
			{
				if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer())
				{
					return true;
				}
				if (!AdminCommandHandler.IsCommand(text))
				{
					return true;
				}
				return !AdminCommandHandler.Handle(sender, text);
			}
			catch (Exception ex)
			{
				if (ModConfig.VerboseLogging.Value)
				{
					Plugin.Log.LogWarning((object)("[ChatPatch] Exception in chat prefix: " + ex.Message));
				}
				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.");
				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;
		}

		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_0029: 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_0030: Expected O, but got Unknown
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Invalid comparison between Unknown and I4
			//IL_0051: 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, (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.");
				Game.instance.SavePlayerProfile(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(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)("[ServerCharacterVault] 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)("[ServerCharacterVault] 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)$"[ServerCharacterVault] 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)
		{
			string value = ConnectionRejectionManager.ConsumeReason();
			if (!string.IsNullOrWhiteSpace(value))
			{
				Traverse.Create((object)__instance).Field("m_connectionFailedError").Property("text", (object[])null)
					.SetValue((object)value);
			}
		}
	}
}
namespace ServerCharacterVault.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 ServerCharacterVault.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)"[ServerCharacterVault :: ProfileHelper] Could not find PlayerProfile.SavePlayerToDisk method via reflection.");
				}
				if (_playerNameField == null || _filenameField == null)
				{
					Plugin.Log.LogWarning((object)"[ServerCharacterVault :: ProfileHelper] Could not find name/filename fields on PlayerProfile.");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)("[ServerCharacterVault :: ProfileHelper] Generating native blank .fch profile for character '" + characterName + "'..."));
			EnsureInitialized();
			if (_savePlayerToDiskMethod == null)
			{
				Plugin.Log.LogError((object)"[ServerCharacterVault :: 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)("[ServerCharacterVault :: ProfileHelper] Activator.CreateInstance failed: " + ex.Message));
					}
				}
				if (val == null)
				{
					Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)$"[ServerCharacterVault :: ProfileHelper] SUCCESS: Generated {array.Length}-byte native blank profile for '{characterName}'.");
					return array;
				}
				Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)("[ServerCharacterVault :: 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)$"[ServerCharacterVault :: ProfileHelper] Failed to generate blank profile for '{characterName}': {ex2}");
				}
				return Array.Empty<byte>();
			}
		}
	}
	internal static class ZNetHelper
	{
		private static readonly FieldInfo? FiPeers;

		private static readonly FieldInfo? FiAdminList;

		private static readonly MethodInfo? MiListContainsId;

		static ZNetHelper()
		{
			FiPeers = typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			FiAdminList = typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			MiListContainsId = typeof(ZNet).GetMethod("ListContainsId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (FiPeers == null)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogWarning((object)"[ZNetHelper] ZNet.m_peers not found — peer enumeration unavailable.");
				}
			}
			if (FiAdminList == null || MiListContainsId == null)
			{
				ManualLogSource log2 = Plugin.Log;
				if (log2 != null)
				{
					log2.LogWarning((object)"[ZNetHelper] Admin list fields not found — admin commands unavailable.");
				}
			}
		}

		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 string GetPlayerId(ZNetPeer peer)
		{
			object obj;
			if (peer == null)
			{
				obj = null;
			}
			else
			{
				ISocket socket = peer.m_socket;
				obj = ((socket != null) ? socket.GetHostName() : null);
			}
			if (obj == null)
			{
				obj = "";
			}
			return (string)obj;
		}

		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;
				}
			}
			if (!playerId.StartsWith("Steam_", StringComparison.Ordinal))
			{
				return playerId.StartsWith("Xbox_", StringComparison.Ordinal);
			}
			return true;
		}

		public static bool IsAdmin(string playerId)
		{
			if ((Object)(object)ZNet.instance == (Object)null || FiAdminList == null || MiListContainsId == null)
			{
				return false;
			}
			try
			{
				object value = FiAdminList.GetValue(ZNet.instance);
				if (value == null)
				{
					return false;
				}
				return (bool)(MiListContainsId.Invoke(ZNet.instance, new object[2] { value, playerId }) ?? ((object)false));
			}
			catch (Exception ex)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogError((object)("[ZNetHelper] IsAdmin check failed: " + ex.Message));
				}
				return false;
			}
		}
	}
}