Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of SavesCharactersOnStop v1.0.2
IronLabs.SavesCharactersOnStop.dll
Decompiled 16 hours agousing 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.SavesCharactersOnStop")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("IronLabs.SavesCharactersOnStop")] [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.38721")] namespace IronLabs.SavesCharactersOnStop { internal sealed class GracefulShutdownCoordinator : IDisposable { internal const string SaveRequestRpc = "SavesCharactersOnStop SaveRequest"; internal const string SaveStartedRpc = "SavesCharactersOnStop SaveStarted"; private const string ExitFilePath = "saves_characters_on_stop.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("saves_characters_on_stop.drp")) { QueueExitRequest(); } } catch (Exception ex) { SavesCharactersOnStopPlugin.Log.LogError("Could not watch saves_characters_on_stop.drp; shutdown requests cannot be detected: " + ex.Message); } } private void TryStartFromExitFile() { if (!CanCoordinateShutdown()) { _exitRequestPending = true; return; } _exitRequestPending = false; try { if (File.Exists("saves_characters_on_stop.drp")) { string text = File.ReadAllText("saves_characters_on_stop.drp").Trim(); if (!int.TryParse(text, out var result) || result != Process.GetCurrentProcess().Id) { File.Delete("saves_characters_on_stop.drp"); SavesCharactersOnStopPlugin.Log.LogWarning("Ignored a stale or invalid saves_characters_on_stop.drp request for process '" + text + "'."); } else { File.Delete("saves_characters_on_stop.drp"); Start(ZNet.instance); } } } catch (Exception ex) { SavesCharactersOnStopPlugin.Log.LogError("Could not process saves_characters_on_stop.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); SavesCharactersOnStopPlugin.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); } SavesCharactersOnStopPlugin.Log.LogMessage($"Shutdown requested; saving {_pendingPeers.Count} connected character(s)."); RequestNextProfiles(); CompleteIfFinished(); } internal bool RequestRestart() { if (!CanCoordinateShutdown() || _requestId != null || _shutdownCommitted) { return false; } Start(ZNet.instance); return true; } 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("SavesCharactersOnStop 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; SavesCharactersOnStopPlugin.Log.LogError("The saves_characters_on_stop.drp watcher failed; shutdown requests may no longer be detected."); } if (_requestId == null) { TryStartFromExitFile(); } } } private FileSystemWatcher CreateExitFileWatcher() { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(Path.GetFullPath("."), "saves_characters_on_stop.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() { SavesCharactersOnStopPlugin.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()); SavesCharactersOnStopPlugin.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); } SavesCharactersOnStopPlugin.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; SavesCharactersOnStopPlugin.Log.LogMessage("Starting the vanilla application shutdown."); SavesCharactersOnStopPlugin.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() { SavesCharactersOnStopPlugin.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>("SavesCharactersOnStop SaveRequest", (Action<ZRpc, string>)SaveProfile); } else { peer.m_rpc.Register<string>("SavesCharactersOnStop SaveStarted", (Action<ZRpc, string>)SavesCharactersOnStopPlugin.Coordinator.RecordSaveStarted); } } private static void SaveProfile(ZRpc serverRpc, string requestId) { Game instance = Game.instance; if (((instance != null) ? instance.GetPlayerProfile() : null) != null) { SavesCharactersOnStopPlugin.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) { SavesCharactersOnStopPlugin.Log.LogWarning("The active ServerCharacters transfer did not finish within 10 seconds; saving anyway."); ServerCharactersTransferTracker.Reset(); break; } yield return null; } SavesCharactersOnStopPlugin.Log.LogMessage("Saving the character for graceful shutdown request " + requestId + "."); serverRpc.Invoke("SavesCharactersOnStop 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) { SavesCharactersOnStopPlugin.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()) { SavesCharactersOnStopPlugin.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); } } internal static class RestartServerCommand { private const string CommandName = "restartserver"; internal static void Register() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) new ConsoleCommand("restartserver", "Saves connected characters and the world, then restarts the dedicated server.", new ConsoleEventFailable(Execute), false, false, true, false, true, (ConsoleOptionsFetcher)null, false, true, true); } private static object Execute(ConsoleEventArgs args) { if (!SavesCharactersOnStopPlugin.RestartCommandEnabled) { return "The restartserver command is disabled on this server."; } if (!SavesCharactersOnStopPlugin.Coordinator.RequestRestart()) { return "A server restart cannot be started in the current state."; } args.Context.AddString("Server restart requested; saving connected characters."); SavesCharactersOnStopPlugin.Log.LogMessage("An administrator requested a graceful server restart."); return true; } } [BepInPlugin("IronLabs.SavesCharactersOnStop", "IronLabs.SavesCharactersOnStop", "1.0.2")] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class SavesCharactersOnStopPlugin : IronLabsPlugin { private const string PluginGuid = "IronLabs.SavesCharactersOnStop"; private const string PluginName = "IronLabs.SavesCharactersOnStop"; private const string PluginVersion = "1.0.2"; private const string DisableRestartCommandSwitch = "--disable-restart-command"; internal static ModLog Log { get; private set; } internal static GracefulShutdownCoordinator Coordinator { get; private set; } internal static SavesCharactersOnStopPlugin Instance { get; private set; } internal static bool RestartCommandEnabled { get; private set; } private void Awake() { Instance = this; Log = InitializePlugin("IronLabs.SavesCharactersOnStop"); RestartCommandEnabled = !Environment.GetCommandLineArgs().Contains("--disable-restart-command"); Coordinator = new GracefulShutdownCoordinator(SynchronizationContext.Current); RestartServerCommand.Register(); Log.LogDebug("The restartserver command is " + (RestartCommandEnabled ? "enabled" : "disabled") + "."); Log.LogInfo("IronLabs.SavesCharactersOnStop 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; RestartCommandEnabled = false; 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}"); } } }