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 _pendingPeers = new HashSet(); private readonly HashSet _requestedPeers = new HashSet(); private readonly Queue _queuedPeers = new Queue(); private readonly Dictionary _startedRequests = new Dictionary(); 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)instance.GetPeers()).FirstOrDefault((Func)((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)_pendingPeers).FirstOrDefault((Func)((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("CharacterVault SaveRequest", (Action)SaveProfile); } else { peer.m_rpc.Register("CharacterVault SaveStarted", (Action)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}"); } } }