Decompiled source of CharacterVault v1.0.2

IronLabs.CharacterVault.dll

Decompiled a day 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.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using System.Threading;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using IronLabs.SharedLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("IronLabs.CharacterVault")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("IronLabs.CharacterVault")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("70BCF48B-1C23-4B2A-8A4F-F01CC21E3EC5")]
[assembly: AssemblyFileVersion("1.0.2")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.2.22328")]
namespace IronLabs.CharacterVault
{
	internal sealed class GracefulShutdownCoordinator : IDisposable
	{
		internal const string SaveRequestRpc = "CharacterVault SaveRequest";

		internal const string SaveStartedRpc = "CharacterVault SaveStarted";

		private const string ExitFilePath = "character_vault.drp";

		private const int MaximumConcurrentSaves = 4;

		private const int ShutdownTimeoutSeconds = 90;

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

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

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

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

		private readonly FileSystemWatcher _exitFileWatcher;

		private readonly SynchronizationContext _unityContext;

		private Timer _timeoutTimer;

		private int _disposed;

		private int _exitRequestQueued;

		private volatile bool _watcherFailed;

		private bool _exitRequestPending;

		private bool _shutdownCommitted;

		private string _requestId;

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

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

		internal void RecordSavedProfile(PlayerProfile profile)
		{
			if (_requestId == null)
			{
				return;
			}
			ZNetPeer val = FindSavedPeer(profile);
			if (val != null && _pendingPeers.Remove(val))
			{
				_startedRequests.Remove(val);
				_requestedPeers.Remove(val);
				CharacterVaultPlugin.Log.LogMessage($"Confirmed graceful character save for {val.m_playerName} ({_pendingPeers.Count} remaining).");
				ZNet.instance.Disconnect(val);
				RequestNextProfiles();
				if (_pendingPeers.Count == 0)
				{
					Complete();
				}
			}
		}

		internal void RecordSaveStarted(ZRpc peerRpc, string startedRequestId)
		{
			ZNet instance = ZNet.instance;
			ZNetPeer val = ((instance != null) ? ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == peerRpc)) : null);
			if (val != null && _pendingPeers.Contains(val) && startedRequestId == _requestId)
			{
				_startedRequests[val] = startedRequestId;
			}
		}

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

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

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

		private void RequestNextProfiles()
		{
			while (_requestedPeers.Count < 4 && _queuedPeers.Count > 0)
			{
				ZNetPeer val = _queuedPeers.Dequeue();
				if (!_pendingPeers.Contains(val) || !HasActiveCharacter(val))
				{
					_pendingPeers.Remove(val);
					continue;
				}
				_requestedPeers.Add(val);
				val.m_rpc.Invoke("CharacterVault SaveRequest", new object[1] { _requestId });
			}
		}

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

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

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

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

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

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

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

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

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

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

		private ZNetPeer FindSavedPeer(PlayerProfile profile)
		{
			string value;
			return ((IEnumerable<ZNetPeer>)_pendingPeers).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => _startedRequests.TryGetValue(candidate, out value) && value == _requestId && ServerCharactersIdentity.Matches(profile, candidate)));
		}

		private void Complete()
		{
			CharacterVaultPlugin.Log.LogMessage("All connected character profiles were written to disk; continuing the vanilla shutdown.");
			ContinueVanillaShutdown();
		}

		private void CompleteAfterTimeout()
		{
			string arg = string.Join(", ", _pendingPeers.Select((ZNetPeer peer) => peer.m_playerName).ToArray());
			CharacterVaultPlugin.Log.LogWarning($"The {90}-second shutdown save timeout expired with " + $"{_pendingPeers.Count} unsaved character(s): {arg}.");
			foreach (ZNetPeer pendingPeer in _pendingPeers)
			{
				ZNet.instance.Disconnect(pendingPeer);
			}
			CharacterVaultPlugin.Log.LogWarning("Continuing the vanilla server shutdown after the character save timeout.");
			ContinueVanillaShutdown();
		}

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

		private static bool HasActiveCharacter(ZNetPeer peer)
		{
			if (peer?.m_rpc != null)
			{
				ISocket socket = peer.m_socket;
				if (socket != null && socket.IsConnected())
				{
					return !string.IsNullOrWhiteSpace(peer.m_playerName);
				}
			}
			return false;
		}
	}
	[HarmonyPatch(typeof(ZNet), "Start")]
	internal static class PendingExitRequestPatch
	{
		private static void Postfix()
		{
			CharacterVaultPlugin.Coordinator?.ProcessPendingExitRequest();
		}
	}
	[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
	internal static class ProfileSaveRequestRpc
	{
		private static void Postfix(ZNet __instance, ZNetPeer peer)
		{
			if (!__instance.IsServer())
			{
				peer.m_rpc.Register<string>("CharacterVault SaveRequest", (Action<ZRpc, string>)SaveProfile);
			}
			else
			{
				peer.m_rpc.Register<string>("CharacterVault SaveStarted", (Action<ZRpc, string>)CharacterVaultPlugin.Coordinator.RecordSaveStarted);
			}
		}

		private static void SaveProfile(ZRpc serverRpc, string requestId)
		{
			Game instance = Game.instance;
			if (((instance != null) ? instance.GetPlayerProfile() : null) != null)
			{
				CharacterVaultPlugin.Instance.Run(SaveWhenReady(serverRpc, requestId));
			}
		}

		private static IEnumerator SaveWhenReady(ZRpc serverRpc, string requestId)
		{
			ServerCharactersTransferTracker.BeginShutdownRequest();
			float deadline = Time.realtimeSinceStartup + 10f;
			while (ServerCharactersTransferTracker.IsSendingProfile)
			{
				if (Time.realtimeSinceStartup >= deadline)
				{
					CharacterVaultPlugin.Log.LogWarning("The active ServerCharacters transfer did not finish within 10 seconds; saving anyway.");
					ServerCharactersTransferTracker.Reset();
					break;
				}
				yield return null;
			}
			CharacterVaultPlugin.Log.LogMessage("Saving the character for graceful shutdown request " + requestId + ".");
			serverRpc.Invoke("CharacterVault SaveStarted", new object[1] { requestId });
			Game.instance.SavePlayerProfile(true);
		}
	}
	[HarmonyPatch(typeof(ZRpc), "Invoke")]
	internal static class ServerCharactersTransferTracker
	{
		private const string ProfileRpc = "ServerCharacters PlayerProfile";

		internal static bool IsSendingProfile { get; private set; }

		internal static void BeginShutdownRequest()
		{
			if (IsSendingProfile)
			{
				CharacterVaultPlugin.Log.LogInfo("Waiting for the active ServerCharacters transfer before the shutdown save.");
			}
		}

		internal static void Reset()
		{
			IsSendingProfile = false;
		}

		private static void Prefix(string method, object[] parameters)
		{
			if (method != "ServerCharacters PlayerProfile" || parameters == null || parameters.Length != 1)
			{
				return;
			}
			object obj = parameters[0];
			ZPackage val = (ZPackage)((obj is ZPackage) ? obj : null);
			if (val == null)
			{
				return;
			}
			int pos = val.GetPos();
			try
			{
				if (val.Size() >= 16)
				{
					val.SetPos(0);
					val.ReadLong();
					int num = val.ReadInt();
					int num2 = val.ReadInt();
					if (num2 > 0 && num >= 0 && num < num2)
					{
						IsSendingProfile = num < num2 - 1;
					}
				}
			}
			finally
			{
				val.SetPos(pos);
			}
		}
	}
	[HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")]
	internal static class SavedProfilePatch
	{
		private static void Postfix(PlayerProfile __instance, bool __result)
		{
			if (__result)
			{
				ZNet instance = ZNet.instance;
				if (instance != null && instance.IsServer())
				{
					CharacterVaultPlugin.Coordinator.RecordSavedProfile(__instance);
				}
			}
		}
	}
	internal static class ServerCharactersIdentity
	{
		internal static bool Matches(PlayerProfile profile, ZNetPeer peer)
		{
			string hostName = peer.m_socket.GetHostName();
			string b = (Regex.IsMatch(hostName, "^\\d+$") ? ("Steam_" + hostName) : hostName) + "_" + peer.m_playerName.ToLower();
			return string.Equals(profile.m_filename, b, StringComparison.Ordinal);
		}
	}
	[BepInPlugin("IronLabs.CharacterVault", "IronLabs.CharacterVault", "1.0.2")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class CharacterVaultPlugin : IronLabsPlugin
	{
		private const string PluginGuid = "IronLabs.CharacterVault";

		private const string PluginName = "IronLabs.CharacterVault";

		private const string PluginVersion = "1.0.2";

		internal static ModLog Log { get; private set; }

		internal static GracefulShutdownCoordinator Coordinator { get; private set; }

		internal static CharacterVaultPlugin Instance { get; private set; }

		private void Awake()
		{
			Instance = this;
			Log = InitializePlugin("IronLabs.CharacterVault");
			Coordinator = new GracefulShutdownCoordinator(SynchronizationContext.Current);
			Log.LogInfo("IronLabs.CharacterVault 1.0.2 is loaded.");
		}

		internal void Run(IEnumerator routine)
		{
			((MonoBehaviour)this).StartCoroutine(routine);
		}

		internal void QuitNextFrame()
		{
			((MonoBehaviour)this).StartCoroutine(QuitAfterCurrentFrame());
		}

		private static IEnumerator QuitAfterCurrentFrame()
		{
			yield return null;
			Application.Quit();
		}

		private void OnDestroy()
		{
			Coordinator?.Dispose();
			Coordinator = null;
			Instance = null;
			ShutdownPlugin();
			Log = null;
		}
	}
}
namespace IronLabs.SharedLib
{
	public abstract class IronLabsPlugin : BaseUnityPlugin
	{
		private Harmony _harmony;

		private bool _patchesApplied;

		protected ModLog InitializePlugin(string pluginGuid)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Expected O, but got Unknown
			ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger);
			Version version = ((object)this).GetType().Assembly.GetName().Version;
			modLog.LogInfo($"AssemblyVersion: {version}.");
			_harmony = new Harmony(pluginGuid);
			PatchOwnNamespace(modLog);
			return modLog;
		}

		protected void PatchOwnNamespace(ModLog log)
		{
			if (_patchesApplied)
			{
				log.LogDebug("Harmony patches are already active; skipping registration.");
				return;
			}
			string text = ((object)this).GetType().Namespace;
			Type[] types = Assembly.GetExecutingAssembly().GetTypes();
			foreach (Type type in types)
			{
				if (type.Namespace == text)
				{
					_harmony.CreateClassProcessor(type).Patch();
				}
			}
			_patchesApplied = true;
			log.LogDebug("Harmony patches were applied for the plugin namespace.");
		}

		protected void ShutdownPlugin()
		{
			if (_patchesApplied)
			{
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
				_patchesApplied = false;
			}
		}
	}
	public sealed class ModLog
	{
		private readonly ManualLogSource _logger;

		public ModLog(ManualLogSource logger)
		{
			_logger = logger;
		}

		public void LogFatal(object message)
		{
			Write((LogLevel)1, message);
		}

		public void LogError(object message)
		{
			Write((LogLevel)2, message);
		}

		public void LogWarning(object message)
		{
			Write((LogLevel)4, message);
		}

		public void LogMessage(object message)
		{
			Write((LogLevel)8, message);
		}

		public void LogInfo(object message)
		{
			Write((LogLevel)16, message);
		}

		public void LogDebug(object message)
		{
			Write((LogLevel)32, message);
		}

		public void Log(LogLevel level, object message)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			Write(level, message);
		}

		private void Write(LogLevel level, object message)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
			_logger.Log(level, (object)$"[{arg}] {message}");
		}
	}
}