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 CharacterVault v1.0.29
Landoria.CharacterVault.dll
Decompiled 4 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Landoria.SharedLib; using PartyCSharpSDK; using PlayFab; using PlayFab.Party; using Splatform; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Landoria.CharacterVault")] [assembly: AssemblyDescription("Stores trusted character profiles and world-scoped online activity on the server.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Landoria.CharacterVault")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("70BCF48B-1C23-4B2A-8A4F-F01CC21E3EC5")] [assembly: AssemblyFileVersion("1.0.29")] [assembly: AssemblyInformationalVersion("1.0.29")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = "")] [assembly: AssemblyVersion("1.0.29.19303")] namespace Landoria.CharacterVault { internal sealed class CharacterVaultSettings { private bool serverInitialized; internal bool AllowMultipleCharacters { get; private set; } = true; internal IReadOnlyList<StartingItem> StartingItems { get; private set; } internal CharacterVaultSettings() { StartingItems = new List<StartingItem>(); } internal void InitializeServer() { if (!serverInitialized && ServerRole.IsDedicatedServer) { string[] commandLineArgs = Environment.GetCommandLineArgs(); AllowMultipleCharacters = CharacterVaultArgumentPolicy.ResolveAllowMultiple(commandLineArgs); StartingItems = ParseItems(CharacterVaultArgumentPolicy.ResolveStartingItems(commandLineArgs)); serverInitialized = true; CharacterVaultPlugin.Log.LogInfo($"Server allowMultipleCharacters={AllowMultipleCharacters}, " + $"startingItemCount={StartingItems.Count}."); } } private static IReadOnlyList<StartingItem> ParseItems(string value) { List<StartingItem> list = new List<StartingItem>(); if (string.IsNullOrWhiteSpace(value)) { return list; } string[] array = value.Split(new char[1] { ',' }); foreach (string text in array) { string[] array2 = text.Split(new char[1] { ':' }); if (array2.Length != 2 || string.IsNullOrWhiteSpace(array2[0]) || !int.TryParse(array2[1].Trim(), out var result) || result <= 0) { throw new InvalidOperationException("Invalid CharacterVault starting item '" + text + "'."); } list.Add(new StartingItem(array2[0].Trim(), result)); } return list; } } public interface ICharacterRestoreProvider { Task<CharacterRestoreResult> RestoreAsync(string platformPlayerId, string playerName, CancellationToken cancellationToken); } public sealed class CharacterRestoreResult { public CharacterRestoreStatus Status { get; } public byte[] Profile { get; } private CharacterRestoreResult(CharacterRestoreStatus status, byte[] profile) { Status = status; Profile = profile; } public static CharacterRestoreResult Restored(byte[] profile) { return new CharacterRestoreResult(CharacterRestoreStatus.Restored, profile ?? throw new ArgumentNullException("profile")); } public static CharacterRestoreResult NotFound() { return new CharacterRestoreResult(CharacterRestoreStatus.NotFound, null); } public static CharacterRestoreResult Failed() { return new CharacterRestoreResult(CharacterRestoreStatus.Failed, null); } } public enum CharacterRestoreStatus { Restored, NotFound, Failed } public static class CharacterRestoreApi { private static readonly object Sync = new object(); private static ICharacterRestoreProvider _provider; public static void Register(ICharacterRestoreProvider provider) { if (provider == null) { throw new ArgumentNullException("provider"); } lock (Sync) { if (_provider != null && _provider != provider) { throw new InvalidOperationException("A character restore provider is already registered."); } _provider = provider; } } public static bool Unregister(ICharacterRestoreProvider provider) { lock (Sync) { if (_provider != provider) { return false; } _provider = null; return true; } } internal static ICharacterRestoreProvider GetProvider() { lock (Sync) { return _provider; } } } internal static class CharacterVaultArgumentPolicy { private const string MultipleArgument = "--charactervault-allow-multiple-characters"; private const string ItemsArgument = "--charactervault-starting-items"; internal static bool ResolveAllowMultiple(string[] arguments) { if (!TryReadValue(arguments, "--charactervault-allow-multiple-characters", out var value)) { return true; } if (!bool.TryParse(value, out var result)) { throw new InvalidOperationException("Command-line switch --charactervault-allow-multiple-characters requires true or false."); } return result; } internal static string ResolveStartingItems(string[] arguments) { if (!TryReadValue(arguments, "--charactervault-starting-items", out var value)) { return ""; } return value; } private static bool TryReadValue(string[] arguments, string name, out string value) { value = ""; bool flag = false; for (int i = 0; i < arguments.Length; i++) { if (string.Equals(arguments[i], name, StringComparison.OrdinalIgnoreCase)) { if (flag || i + 1 >= arguments.Length) { throw new InvalidOperationException("Command-line switch " + name + " is missing or duplicated."); } value = arguments[++i]; flag = true; } } return flag; } } internal interface ICharacterProfileCatalog { bool HasProfile(string accountId); } internal enum CharacterAdmission { ExistingProfile, NewEnrollment, RejectUnregisteredProfile, RejectAdditionalCharacter, RejectConcurrentEnrollment } internal static class CharacterAdmissionPolicy { internal static CharacterAdmission Decide(bool hasStoredProfile, bool createdThisSession, bool allowMultipleCharacters, bool accountHasProfile, bool enrollmentAvailable) { if (hasStoredProfile) { return CharacterAdmission.ExistingProfile; } if (!createdThisSession) { return CharacterAdmission.RejectUnregisteredProfile; } if (!allowMultipleCharacters && accountHasProfile) { return CharacterAdmission.RejectAdditionalCharacter; } if (!enrollmentAvailable) { return CharacterAdmission.RejectConcurrentEnrollment; } return CharacterAdmission.NewEnrollment; } } internal static class CharacterAdmissionMessages { internal static string ForRejection(CharacterAdmission admission, IReadOnlyList<string> existingProfileNames) { if (admission == CharacterAdmission.RejectAdditionalCharacter && existingProfileNames != null && existingProfileNames.Count > 0) { string text = string.Join(", ", existingProfileNames.Select((string name) => name.ToUpperInvariant())); if (existingProfileNames.Count != 1) { return "You already have characters: " + text + ". You cannot create more."; } return "You already have a character: " + text + ". You cannot create more."; } if (admission != CharacterAdmission.RejectUnregisteredProfile) { return "This platform account already has a character."; } if (existingProfileNames == null || existingProfileNames.Count == 0) { return "Create a new character before joining this server."; } return ((existingProfileNames.Count == 1) ? "Create a new character or use the previously used one: " : "Create a new character or use one of the previously used ones: ") + string.Join(", ", existingProfileNames.Select((string name) => name.ToUpperInvariant())) + "."; } } internal sealed class CharacterAdmissionEvaluator { private readonly ICharacterProfileCatalog _profiles; internal CharacterAdmissionEvaluator(ICharacterProfileCatalog profiles) { _profiles = profiles; } internal CharacterAdmission Decide(bool hasStoredProfile, string accountId, bool createdThisSession, bool allowMultipleCharacters, bool enrollmentAvailable) { bool accountHasProfile = !hasStoredProfile && createdThisSession && !allowMultipleCharacters && _profiles.HasProfile(accountId); return CharacterAdmissionPolicy.Decide(hasStoredProfile, createdThisSession, allowMultipleCharacters, accountHasProfile, enrollmentAvailable); } } internal sealed class ServerProfileSessionState { internal bool CanSave { get { if (Verified && Admitted) { return Permitted; } return false; } } internal bool PermissionChecked { get; private set; } internal bool Verified { get; set; } internal bool Admitted { get; set; } internal bool Permitted { get; private set; } internal void RecordPermission(bool permitted) { PermissionChecked = true; Permitted = permitted; } } internal static class SaveAcknowledgementPolicy { internal static bool CanAcknowledge(ServerProfileSessionState session) { return session?.CanSave ?? false; } } internal static class CharacterVaultRejection { internal const string MessageRpc = "CharacterVault_Rejection_v1"; internal const string AckRpc = "CharacterVault_RejectionAck_v1"; private const float DisconnectFallbackSeconds = 2f; private static readonly Dictionary<ZRpc, float> Deadlines = new Dictionary<ZRpc, float>(); private static readonly HashSet<ZRpc> DisconnectRequested = new HashSet<ZRpc>(); private static readonly HashSet<string> PermittedListRejections = new HashSet<string>(); internal static void RegisterServer(ZRpc rpc) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Expected O, but got Unknown rpc.Register("CharacterVault_RejectionAck_v1", new Method(ReceiveAck)); } internal static void RegisterClient(ZRpc rpc) { ClearClient(); rpc.Register<ZPackage>("CharacterVault_Rejection_v1", (Action<ZRpc, ZPackage>)ReceiveMessage); } internal static void Reject(ZRpc rpc, string message, string systemMessage = null) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown CharacterVaultPlugin.Log.LogWarning("CharacterVault rejected " + rpc.GetSocket().GetHostName() + ": " + message); Deadlines[rpc] = Time.unscaledTime + 2f; ZPackage val = new ZPackage(); val.Write(message); val.Write(systemMessage ?? message); rpc.Invoke("CharacterVault_Rejection_v1", new object[1] { val }); } internal static void RecordPermittedListRejection(string hostName) { PermittedListRejections.Add(hostName); } internal static void SendPermittedListRejection(ZRpc rpc) { object obj; if (rpc == null) { obj = null; } else { ISocket socket = rpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (text != null && PermittedListRejections.Remove(text)) { Reject(rpc, "This platform account is not allowed to join this server."); } } internal static void SetClientMessage(string userMessage, string systemMessage = null) { ConnectionFailureMessages.Push("Landoria.CharacterVault", userMessage, systemMessage); } internal static void ClearPendingClientMessage() { ClearClient(); } internal static void Remove(ZRpc rpc) { Deadlines.Remove(rpc); DisconnectRequested.Remove(rpc); } internal static void Tick() { DisconnectExpired(); } internal static void Clear() { Deadlines.Clear(); DisconnectRequested.Clear(); PermittedListRejections.Clear(); ClearClient(); } private static void ReceiveMessage(ZRpc rpc, ZPackage package) { string text = package.ReadString(); string text2 = package.ReadString(); ConnectionFailureMessages.Push("Landoria.CharacterVault", text, text2); CharacterVaultPlugin.Log.LogWarning("Server rejected the character: " + text + " System detail: " + text2); rpc.Invoke("CharacterVault_RejectionAck_v1", Array.Empty<object>()); CharacterVaultPlugin.Log.LogDebug("Acknowledged the CharacterVault rejection; waiting for the server disconnect."); } private static void ReceiveAck(ZRpc rpc) { if (Deadlines.ContainsKey(rpc)) { RequestDisconnect(rpc); } } private static void DisconnectExpired() { ZRpc[] array = (from entry in Deadlines where Time.unscaledTime >= entry.Value select entry.Key).ToArray(); foreach (ZRpc val in array) { if (DisconnectRequested.Contains(val)) { ForceDisconnect(val); } else { RequestDisconnect(val); } } } private static void RequestDisconnect(ZRpc rpc) { DisconnectRequested.Add(rpc); Deadlines[rpc] = Time.unscaledTime + 2f; CharacterVaultPlugin.Log.LogDebug("Requesting rejected pre-spawn client disconnection."); rpc.Invoke("Disconnect", Array.Empty<object>()); } private static void ForceDisconnect(ZRpc rpc) { Deadlines.Remove(rpc); DisconnectRequested.Remove(rpc); ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == rpc)) : null); if (val != null) { CharacterVaultPlugin.Log.LogWarning("Rejected client did not disconnect; closing the server connection."); ZNet.instance.Disconnect(val); } } private static void ClearClient() { ConnectionFailureMessages.Clear("Landoria.CharacterVault"); } } internal static class CharacterRejectionMessages { internal const string PermittedListDenied = "This platform account is not allowed to join this server."; internal const string AdditionalCharacterDenied = "This platform account already has a character."; } internal sealed class CharacterSaveStatusDisplay { private const float ActiveDisplaySeconds = 30f; private const float CommitTimeoutSeconds = 20f; private const float ResultDisplaySeconds = 3f; private readonly SaveStatusLifecycle _lifecycle = new SaveStatusLifecycle(); private TextMeshProUGUI _label; internal void Attach(Minimap minimap) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_label != (Object)null) && !((Object)(object)minimap?.m_mapImageSmall == (Object)null) && !((Object)(object)minimap.m_biomeNameSmall == (Object)null)) { GameObject val = new GameObject("CharacterVaultSaveStatus", new Type[1] { typeof(RectTransform) }); val.transform.SetParent(minimap.m_smallRoot.transform, false); RectTransform component = val.GetComponent<RectTransform>(); component.anchorMin = new Vector2(0.5f, 0.5f); component.anchorMax = new Vector2(0.5f, 0.5f); component.pivot = new Vector2(0.5f, 1f); component.sizeDelta = new Vector2(480f, 30f); RectTransform rectTransform = ((Graphic)minimap.m_mapImageSmall).rectTransform; Rect rect = rectTransform.rect; float x = ((Rect)(ref rect)).center.x; rect = rectTransform.rect; ((Transform)component).position = ((Transform)rectTransform).TransformPoint(new Vector3(x, ((Rect)(ref rect)).yMin - 8f, 0f)); ((Transform)component).SetAsLastSibling(); _label = val.AddComponent<TextMeshProUGUI>(); ((TMP_Text)_label).font = minimap.m_biomeNameSmall.font; ((TMP_Text)_label).fontSharedMaterial = minimap.m_biomeNameSmall.fontSharedMaterial; ((TMP_Text)_label).fontSize = minimap.m_biomeNameSmall.fontSize; ((TMP_Text)_label).fontSizeMax = minimap.m_biomeNameSmall.fontSize; ((TMP_Text)_label).fontSizeMin = 8f; ((TMP_Text)_label).enableAutoSizing = true; ((TMP_Text)_label).textWrappingMode = (TextWrappingModes)0; ((TMP_Text)_label).fontStyle = (FontStyles)1; ((TMP_Text)_label).alignment = (TextAlignmentOptions)514; ((Graphic)_label).color = Color.white; ((Graphic)_label).raycastTarget = false; ((TMP_Text)_label).text = string.Empty; val.SetActive(false); } } internal void ShowSaving(string requestId) { Show(requestId, "Saving character...", 30f, waitingForCommit: false); } internal void ShowAccepted(string requestId) { int version = Show(requestId, "Saving character......", 30f, waitingForCommit: true); CharacterVaultPlugin.Instance?.Run(FailWithoutCommit(requestId, version)); } internal void ShowCommitted(string requestId) { if (_lifecycle.CanCommit(requestId)) { Show(requestId, "Character saved", 3f, waitingForCommit: false); } } internal void Hide() { _lifecycle.Clear(); if ((Object)(object)_label != (Object)null) { ((Component)_label).gameObject.SetActive(false); } } internal void Dispose() { _lifecycle.Clear(); if ((Object)(object)_label != (Object)null) { Object.Destroy((Object)(object)((Component)_label).gameObject); _label = null; } } private int Show(string requestId, string message, float duration, bool waitingForCommit) { int num = _lifecycle.Begin(requestId, waitingForCommit); Attach(Minimap.instance); if ((Object)(object)_label == (Object)null) { return num; } ((TMP_Text)_label).text = message; ((Component)_label).gameObject.SetActive(true); CharacterVaultPlugin.Instance?.Run(HideAfterDelay(num, duration)); return num; } private IEnumerator FailWithoutCommit(string requestId, int version) { yield return (object)new WaitForSecondsRealtime(20f); if (_lifecycle.CanFail(requestId, version)) { Show(requestId, "Failed", 3f, waitingForCommit: false); } } private IEnumerator HideAfterDelay(int version, float duration) { yield return (object)new WaitForSecondsRealtime(duration); if (_lifecycle.IsCurrent(version)) { Hide(); } } } internal sealed class ClientSaveLifecycle { private bool _active; private bool _enrolling; private bool _spawned; internal bool IsActive => _active; internal bool CanUpload { get { if (_active) { return _spawned; } return false; } } internal bool IsEnrolling => _enrolling; internal bool HasSpawned => _spawned; internal void ActivateExisting() { _active = true; } internal void BeginEnrollment() { _active = true; _enrolling = true; } internal bool RecordSpawn(bool isLocalPlayer) { if (!isLocalPlayer) { return false; } _spawned = true; bool enrolling = _enrolling; _enrolling = false; return enrolling; } internal void Reset() { _active = false; _enrolling = false; _spawned = false; } } internal sealed class ServerFinalSaveMonitor { private sealed class PendingDisconnect { internal float Deadline { get; } internal string PlayerName { get; } internal PendingDisconnect(string playerName, float deadline) { PlayerName = playerName; Deadline = deadline; } } private const float TimeoutSeconds = 10f; private readonly Dictionary<ZRpc, PendingDisconnect> _pending = new Dictionary<ZRpc, PendingDisconnect>(); private readonly HashSet<ZRpc> _receivedFinalSaves = new HashSet<ZRpc>(); private readonly HashSet<ZRpc> _warned = new HashSet<ZRpc>(); internal void Observe(ZRpc rpc, string playerName, bool connected) { if (rpc != null) { if (connected) { _pending.Remove(rpc); _warned.Remove(rpc); } else if (!_receivedFinalSaves.Contains(rpc) && !_pending.ContainsKey(rpc) && !_warned.Contains(rpc)) { _pending[rpc] = new PendingDisconnect(playerName, Time.realtimeSinceStartup + 10f); } } } internal void RecordSaveReceived(ZRpc rpc, string requestId) { if (IsFinalDisconnectRequest(requestId)) { _receivedFinalSaves.Add(rpc); _pending.Remove(rpc); _warned.Remove(rpc); } } private static bool IsFinalDisconnectRequest(string requestId) { if (requestId == null || !requestId.StartsWith("disconnect-", StringComparison.Ordinal)) { return requestId?.StartsWith("server-disconnect-", StringComparison.Ordinal) ?? false; } return true; } internal void Update() { foreach (ZRpc item in new List<ZRpc>(_pending.Keys)) { PendingDisconnect pendingDisconnect = _pending[item]; if (!(Time.realtimeSinceStartup < pendingDisconnect.Deadline)) { _pending.Remove(item); _warned.Add(item); CharacterVaultPlugin.Log.LogWarning("No final character save was received from " + pendingDisconnect.PlayerName + " " + $"within {10f:0} seconds after the connection was lost."); } } } internal void RecordRemoved(ZRpc rpc, string playerName) { Observe(rpc, playerName, connected: false); _receivedFinalSaves.Remove(rpc); } internal void Clear() { _pending.Clear(); _receivedFinalSaves.Clear(); _warned.Clear(); } } internal static class NewCharacterPolicy { internal static bool HasNeverJoinedAWorld(PlayerProfile profile) { if (profile.m_firstSpawn) { return profile.m_playerStats[0].m_knownWorlds.Count == 0; } return false; } } internal static class ProfileFile { internal static byte[] Read(PlayerProfile profile) { //IL_0007: 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_0013: Expected O, but got Unknown FileReader val = new FileReader(profile.GetPath(), profile.m_fileSource, (FileHelperType)0); try { Stream baseStream = val.m_binary.BaseStream; baseStream.Position = 0L; byte[] array = new byte[baseStream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = baseStream.Read(array, i, array.Length - i); if (num == 0) { throw new EndOfStreamException("The character profile ended unexpectedly."); } } return array; } finally { val.Dispose(); } } internal static PlayerProfile ReplaceSelected(byte[] data) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); string path = playerProfile.GetPath(); string text = path + ".vault-new"; Write(text, playerProfile.m_fileSource, data); SaveApiCompatibility.ReplaceOldFile(path, text, playerProfile.m_fileSource); SaveApiCompatibility.InvalidateCharacterCache(); PlayerProfile val = new PlayerProfile(playerProfile.GetFilename(), playerProfile.m_fileSource); if (!val.Load()) { throw new InvalidDataException("Valheim rejected the authoritative server profile."); } return val; } private static void Write(string path, FileSource source, byte[] data) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Invalid comparison between Unknown and I4 FileWriter obj = SaveApiCompatibility.CreateWriter(path, source); obj.m_binary.Write(data); obj.Finish(); if ((int)obj.Status != 2) { throw new IOException("The authoritative character profile could not be written."); } } } internal static class SaveApiCompatibility { private static readonly Type[] LegacyWriterParameters = new Type[3] { typeof(string), typeof(FileHelperType), typeof(FileSource) }; private static readonly Type[] CurrentWriterParameters = new Type[4] { typeof(string), typeof(CloudStorageFileGrouping), typeof(FileHelperType), typeof(FileSource) }; internal static FileSource LocalSource => (FileSource)Enum.Parse(typeof(FileSource), "Local"); internal static FileWriter CreateWriter(string path, FileSource source) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown ConstructorInfo constructor = typeof(FileWriter).GetConstructor(CurrentWriterParameters); if (!(constructor != null)) { constructor = typeof(FileWriter).GetConstructor(LegacyWriterParameters) ?? throw new MissingMethodException(typeof(FileWriter).FullName, ".ctor"); return (FileWriter)constructor.Invoke(new object[3] { path, (object)(FileHelperType)0, source }); } return (FileWriter)constructor.Invoke(new object[4] { path, (object)(CloudStorageFileGrouping)0, (object)(FileHelperType)0, source }); } internal static void ReplaceOldFile(string path, string next, FileSource source) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) MethodInfo methodInfo = FindMethod(typeof(FileHelpers), "ReplaceOldFile", typeof(string), typeof(string), typeof(string), typeof(CloudStorageFileGrouping), typeof(FileSource)); object[] parameters = ((!(methodInfo != null)) ? new object[4] { path, next, path + ".old", source } : new object[5] { path, next, path + ".old", (object)(CloudStorageFileGrouping)0, source }); (methodInfo ?? RequireMethod(typeof(FileHelpers), "ReplaceOldFile", typeof(string), typeof(string), typeof(string), typeof(FileSource))).Invoke(null, parameters); } internal static void InvalidateCharacterCache() { MethodInfo methodInfo = FindMethod(typeof(SaveSystem), "InvalidateCache", typeof(SaveDataType)); if (methodInfo != null) { methodInfo.Invoke(null, new object[1] { (object)(SaveDataType)1 }); } else { RequireMethod(typeof(SaveSystem), "InvalidateCache").Invoke(null, null); } } internal static string GetCharacterPath(FileSource source, string filename) { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) MethodInfo methodInfo = FindMethod(typeof(SaveSystem), "GetCharacterPath", typeof(FileSource), typeof(string)); if (methodInfo != null) { return (string)methodInfo.Invoke(null, new object[2] { source, filename }); } return (string)RequireMethod(typeof(PlayerProfile), "GetPath", typeof(FileSource), typeof(string)).Invoke(null, new object[2] { source, filename }); } private static MethodInfo FindMethod(Type type, string name, params Type[] parameters) { return type.GetMethod(name, BindingFlags.Static | BindingFlags.Public, null, parameters, null); } private static MethodInfo RequireMethod(Type type, string name, params Type[] parameters) { return FindMethod(type, name, parameters) ?? throw new MissingMethodException(type.FullName, name); } } internal static class SaveStatusMessages { internal const string Saving = "Saving character..."; internal const string Accepted = "Saving character......"; internal const string Saved = "Character saved"; internal const string Failed = "Failed"; } internal sealed class SaveStatusLifecycle { private string _requestId = string.Empty; private bool _waitingForCommit; internal int Version { get; private set; } internal int Begin(string requestId, bool waitingForCommit) { Version++; _requestId = requestId; _waitingForCommit = waitingForCommit; return Version; } internal bool CanCommit(string requestId) { if (_requestId == requestId) { return _waitingForCommit; } return false; } internal bool CanFail(string requestId, int version) { if (Version == version && _requestId == requestId) { return _waitingForCommit; } return false; } internal bool IsCurrent(int version) { return Version == version; } internal void Clear() { Version++; _requestId = string.Empty; _waitingForCommit = false; } } internal sealed class VaultSession { internal string AccountId { get; } internal long CharacterId { get; } internal string Name { get; } internal bool NewCharacter { get; } internal ServerProfileSessionState State { get; } = new ServerProfileSessionState(); internal bool Enrolling { get; set; } internal VaultSession(string accountId, long characterId, string name, bool newCharacter) { AccountId = accountId; CharacterId = characterId; Name = name; NewCharacter = newCharacter; } } internal sealed class PendingCommit { internal byte[] Data { get; } internal ZRpc Rpc { get; } internal string RequestId { get; } internal VaultSession Session { get; } internal PendingCommit(ZRpc rpc, VaultSession session, string requestId, byte[] data) { Rpc = rpc; Session = session; RequestId = requestId; Data = data; } } internal sealed class ProfileCommitQueue { private readonly Queue<PendingCommit> _commits = new Queue<PendingCommit>(); private readonly object _lock = new object(); private readonly VaultStorage _storage; private readonly SynchronizationContext _unityContext; private readonly Action<PendingCommit> _confirm; private bool _workerRunning; internal ProfileCommitQueue(VaultStorage storage, SynchronizationContext unityContext, Action<PendingCommit> confirm) { _storage = storage ?? throw new ArgumentNullException("storage"); _unityContext = unityContext ?? throw new ArgumentNullException("unityContext"); _confirm = confirm ?? throw new ArgumentNullException("confirm"); } internal void Enqueue(PendingCommit commit) { lock (_lock) { _commits.Enqueue(commit); if (!_workerRunning) { _workerRunning = true; ThreadPool.QueueUserWorkItem(delegate { Process(); }); } } } private void Process() { while (true) { if (!TryDequeue(out var commit)) { break; } try { _storage.Commit(commit.Session.AccountId, commit.Session.Name, commit.Data); _unityContext.Post(delegate { _confirm(commit); }, null); } catch (Exception arg) { CharacterVaultPlugin.Log.LogError($"Character vault commit failed: {arg}"); } } } private bool TryDequeue(out PendingCommit commit) { lock (_lock) { commit = ((_commits.Count > 0) ? _commits.Dequeue() : null); if (commit == null) { _workerRunning = false; } return commit != null; } } } internal static class ProfileTransferProtocol { internal const int ChunkSize = 65536; internal static ZPackage Begin(string transferId, int length, string hash) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: 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_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(transferId); val.Write(length); val.Write(hash); return val; } internal static ZPackage Chunk(string transferId, byte[] data, int offset) { //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_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown int num = Math.Min(65536, data.Length - offset); byte[] array = new byte[num]; Buffer.BlockCopy(data, offset, array, 0, num); ZPackage val = new ZPackage(); val.Write(transferId); val.Write(offset); val.Write(array); return val; } } internal sealed class IncomingTransfer { private readonly byte[] _data; private readonly bool[] _blocks; private readonly string _hash; private readonly string _transferId; internal string RequestId { get; set; } private IncomingTransfer(string transferId, int length, string hash) { _transferId = transferId; _hash = hash; _data = new byte[length]; _blocks = new bool[(length + 65536 - 1) / 65536]; } internal static IncomingTransfer Create(ZPackage package, int maximumLength) { string text = package.ReadString(); int num = package.ReadInt(); string text2 = package.ReadString(); if (string.IsNullOrWhiteSpace(text) || num <= 0 || num > maximumLength || text2.Length != 64) { throw new InvalidDataException("The profile transfer header is invalid."); } return new IncomingTransfer(text, num, text2); } internal void Add(ZPackage package) { string text = package.ReadString(); int num = package.ReadInt(); byte[] array = package.ReadByteArray(); if (text != _transferId || num < 0 || num % 65536 != 0 || array.Length == 0 || array.Length > 65536 || num + array.Length > _data.Length) { throw new InvalidDataException("The profile transfer chunk is invalid."); } int num2 = num / 65536; if (_blocks[num2]) { throw new InvalidDataException("The profile transfer contains a duplicate chunk."); } Buffer.BlockCopy(array, 0, _data, num, array.Length); _blocks[num2] = true; } internal byte[] Complete(string transferId) { if (transferId != _transferId || _blocks.Any((bool block) => !block) || !string.Equals(VaultStorage.Hash(_data), _hash, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("The profile transfer is incomplete or corrupted."); } return _data; } } internal sealed class ProfileTransferService : IDisposable { internal const string HelloRpc = "CharacterVault_Hello_v1"; internal const string AdmissionRpc = "CharacterVault_Admission_v1"; internal const string DownloadBeginRpc = "CharacterVault_DownloadBegin_v1"; internal const string DownloadChunkRpc = "CharacterVault_DownloadChunk_v1"; internal const string DownloadCompleteRpc = "CharacterVault_DownloadComplete_v1"; internal const string UploadBeginRpc = "CharacterVault_UploadBegin_v1"; internal const string UploadChunkRpc = "CharacterVault_UploadChunk_v1"; internal const string UploadCompleteRpc = "CharacterVault_UploadComplete_v1"; internal const string SaveRequestRpc = "CharacterVault_SaveRequest_v1"; internal const string SaveAckRpc = "CharacterVault_SaveAck_v1"; internal const string CommitAckRpc = "CharacterVault_CommitAck_v1"; private const int MaximumProfileBytes = 67108864; private readonly Dictionary<ZRpc, VaultSession> _sessions = new Dictionary<ZRpc, VaultSession>(); private readonly Dictionary<ZRpc, IncomingTransfer> _uploads = new Dictionary<ZRpc, IncomingTransfer>(); private readonly Dictionary<string, ZRpc> _enrollments = new Dictionary<string, ZRpc>(StringComparer.Ordinal); private readonly ClientSaveLifecycle _clientLifecycle = new ClientSaveLifecycle(); private readonly VaultStorage _storage = new VaultStorage(); private readonly CharacterAdmissionEvaluator _admission; private readonly ProfileCommitQueue _commits; private readonly ServerFinalSaveMonitor _finalSaveMonitor = new ServerFinalSaveMonitor(); private IncomingTransfer _download; private bool _clientUploadBusy; private bool _suppressNextClientUpload; private string _pendingRequest; private PlayerProfile _pendingProfile; private IReadOnlyList<StartingItem> _serverStartingItems = Array.Empty<StartingItem>(); internal ProfileTransferService(SynchronizationContext unityContext) { if (unityContext == null) { throw new ArgumentNullException("unityContext"); } _admission = new CharacterAdmissionEvaluator(_storage); _commits = new ProfileCommitQueue(_storage, unityContext, ConfirmBackgroundCommit); } internal void Register(ZNet network, ZNetPeer peer) { if (network.IsServer()) { RegisterServer(peer.m_rpc); } else { RegisterClient(peer.m_rpc); } } internal void SendHello(ZRpc serverRpc) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown Game instance = Game.instance; PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null); if (val != null) { ZPackage val2 = new ZPackage(); val2.Write(val.GetPlayerID()); val2.Write(val.GetName()); val2.Write(NewCharacterPolicy.HasNeverJoinedAWorld(val)); serverRpc.Invoke("CharacterVault_Hello_v1", new object[1] { val2 }); } } internal bool Approve(ZRpc rpc) { if (!_sessions.TryGetValue(rpc, out var value)) { Reject(rpc, "Character verification did not complete. Please try again."); return false; } if (value.State.Verified) { return value.State.Admitted; } value.State.Verified = true; if (_storage.TryRead(value.AccountId, value.Name, out var data)) { SendDownload(rpc, value, data); value.State.Admitted = true; return true; } CharacterRestoreResult characterRestoreResult = TryRestore(value); if (characterRestoreResult != null && characterRestoreResult.Status == CharacterRestoreStatus.Restored) { if (!ValidateProfile(rpc, value, characterRestoreResult.Profile)) { return false; } _storage.Commit(value.AccountId, value.Name, characterRestoreResult.Profile); SendDownload(rpc, value, characterRestoreResult.Profile); value.State.Admitted = true; return true; } if (characterRestoreResult != null && characterRestoreResult.Status == CharacterRestoreStatus.Failed) { _sessions.Remove(rpc); Reject(rpc, "Your saved character could not be restored right now. Please try again in a moment."); return false; } value.State.Admitted = AdmitEnrollment(rpc, value); return value.State.Admitted; } internal void RecordPermission(string hostName, bool permitted) { foreach (VaultSession item in _sessions.Values.Where((VaultSession candidate) => string.Equals(candidate.AccountId, hostName, StringComparison.Ordinal))) { item.State.RecordPermission(permitted); } } internal void Remove(ZNetPeer peer) { if (peer?.m_rpc != null) { if (_sessions.TryGetValue(peer.m_rpc, out var value)) { _finalSaveMonitor.RecordRemoved(peer.m_rpc, value.Name); } _sessions.Remove(peer.m_rpc); _uploads.Remove(peer.m_rpc); ReleaseEnrollment(peer.m_rpc); CharacterVaultPlugin.ServerDisconnects?.RecordDisconnected(peer.m_rpc); ZNet instance = ZNet.instance; if (instance != null && !instance.IsServer()) { ResetClientState(); } } } internal string DescribeDisconnect(ZNetPeer peer, bool server) { ZRpc val = peer?.m_rpc; VaultSession value = null; bool flag = val != null && _sessions.TryGetValue(val, out value); ServerProfileSessionState serverProfileSessionState = (flag ? value.State : null); bool flag2 = ((!server) ? (CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false) : (CharacterVaultPlugin.ServerDisconnects?.HasPendingSave(val) ?? false)); return string.Format("side={0}, peerReady={1}, ", server ? "server" : "client", peer != null && peer.IsReady()) + $"sessionTracked={flag}, verified={serverProfileSessionState?.Verified ?? false}, " + $"admitted={serverProfileSessionState?.Admitted ?? false}, permissionChecked={serverProfileSessionState?.PermissionChecked ?? false}, " + $"permitted={serverProfileSessionState?.Permitted ?? false}, canSave={serverProfileSessionState?.CanSave ?? false}, " + $"clientActive={_clientLifecycle.IsActive}, enrolling={_clientLifecycle.IsEnrolling}, " + $"spawned={_clientLifecycle.HasSpawned}, uploadBusy={_clientUploadBusy}, " + $"incomingUpload={val != null && _uploads.ContainsKey(val)}, " + $"incomingDownload={_download != null}, pendingSave={flag2}"; } private static CharacterRestoreResult TryRestore(VaultSession session) { ICharacterRestoreProvider provider = CharacterRestoreApi.GetProvider(); if (provider == null) { return null; } using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10.0)); try { CharacterRestoreResult result = provider.RestoreAsync(session.AccountId, session.Name, cancellationTokenSource.Token).GetAwaiter().GetResult(); if (result != null && result.Status == CharacterRestoreStatus.Restored && (result.Profile == null || result.Profile.Length == 0 || result.Profile.Length > 67108864)) { return CharacterRestoreResult.Failed(); } return result ?? CharacterRestoreResult.Failed(); } catch (Exception ex) { CharacterVaultPlugin.Log.LogWarning("Character restore failed: " + ex); return CharacterRestoreResult.Failed(); } } internal void MonitorFinalSaves() { ZNet instance = ZNet.instance; if (instance == null || !instance.IsServer()) { return; } foreach (KeyValuePair<ZRpc, VaultSession> session in _sessions) { if (session.Value.State.CanSave) { ServerFinalSaveMonitor finalSaveMonitor = _finalSaveMonitor; ZRpc key = session.Key; string name = session.Value.Name; ISocket socket = session.Key.GetSocket(); finalSaveMonitor.Observe(key, name, socket != null && socket.IsConnected()); } } _finalSaveMonitor.Update(); } internal void RequestWorldCheckpoint() { ZNet instance = ZNet.instance; if (instance == null || !instance.IsServer()) { return; } string requestId = "world-" + Guid.NewGuid().ToString("N"); foreach (ZNetPeer item in ZNet.instance.GetPeers().Where(IsReady)) { RequestSave(item, requestId); } } internal void RequestSave(ZNetPeer peer, string requestId) { if (peer?.m_rpc != null && CanSave(_sessions, peer.m_rpc)) { peer.m_rpc.Invoke("CharacterVault_SaveRequest_v1", new object[1] { requestId }); } } internal bool CanRequestSave(ZNetPeer peer) { if (peer?.m_rpc != null) { return CanSave(_sessions, peer.m_rpc); } return false; } internal KickSaveEligibility GetKickSaveEligibility(ZNetPeer peer) { if (peer?.m_rpc != null && (!peer.IsReady() || ((ZDOID)(ref peer.m_characterID)).IsNone())) { return KickSaveEligibility.Rejected; } if (peer?.m_rpc == null || !_sessions.TryGetValue(peer.m_rpc, out var value)) { return KickSaveEligibility.Unmanaged; } if (value.State.CanSave) { return KickSaveEligibility.SaveRequired; } if (!value.State.Verified || !value.State.Admitted || !value.State.PermissionChecked || value.State.Permitted) { return KickSaveEligibility.Unmanaged; } return KickSaveEligibility.Rejected; } internal bool SaveManualClientProfile() { if (_clientLifecycle.IsActive) { ZNet instance = ZNet.instance; if (instance != null && !instance.IsServer() && !((Object)(object)Game.instance == (Object)null)) { Game.instance.SavePlayerProfile(true, false); return true; } } return false; } internal void UploadSavedProfile(PlayerProfile profile) { if (_suppressNextClientUpload) { _suppressNextClientUpload = false; CharacterVaultPlugin.Log.LogInfo("Skipped the redundant local save upload after a confirmed voluntary disconnect save."); } else { if (!_clientLifecycle.IsActive) { return; } ZNet instance = ZNet.instance; if (instance == null || instance.IsServer()) { return; } if (!_clientLifecycle.CanUpload) { CharacterVaultPlugin.Log.LogInfo("Skipped the server upload for a local save before Player.OnSpawned completed."); return; } ZRpc serverRPC = ZNet.instance.GetServerRPC(); if (serverRPC == null) { ResetClientState(); return; } string text = _pendingRequest ?? ("save-" + Guid.NewGuid().ToString("N")); _pendingRequest = null; if (_clientUploadBusy) { _pendingRequest = text; CharacterVaultPlugin.Log.LogInfo("Queued character save request " + text + " while another upload is awaiting confirmation."); return; } byte[] data = ProfileFile.Read(profile); _clientUploadBusy = true; CharacterVaultPlugin.SaveStatus?.ShowSaving(text); CharacterVaultPlugin.Log.LogInfo("Uploading character profile " + profile.GetName() + " for save request " + text + "."); CharacterVaultPlugin.Instance.Run(SendUpload(serverRPC, profile, data, text)); } } internal bool BeginFinalDisconnectSave(string requestId) { Game instance = Game.instance; PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null); if (_clientLifecycle.IsActive) { ZNet instance2 = ZNet.instance; if (instance2 != null && !instance2.IsServer() && val != null) { _pendingRequest = requestId; if (_clientUploadBusy) { CharacterVaultPlugin.Log.LogInfo("Final save request " + requestId + " is waiting for the active upload to finish."); return true; } CharacterVaultPlugin.Log.LogInfo("Writing the final local profile for " + val.GetName() + " before disconnect."); Game.instance.SavePlayerProfile(true, false); return true; } } return false; } internal void SuppressRedundantDisconnectUpload() { _suppressNextClientUpload = true; } internal void RecordPlayerSpawned(Player player) { if (!((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer)) { StartingItemGrantPolicy.ApplyEnrollment(_clientLifecycle, isLocalPlayer: true, _serverStartingItems, FindItem, (GameObject prefab, int quantity) => ((Humanoid)player).GetInventory().AddItem(prefab, quantity), delegate { Game.instance.SavePlayerProfile(true, false); }, delegate(StartingItem item) { CharacterVaultPlugin.Log.LogError($"Could not grant starting item {item.Prefab}:{item.Quantity}."); }); } } internal void ApplyPendingProfile(ref PlayerProfile profile) { if (_pendingProfile != null) { profile = _pendingProfile; _pendingProfile = null; } } public void Dispose() { _sessions.Clear(); _uploads.Clear(); _enrollments.Clear(); _finalSaveMonitor.Clear(); _download = null; } private void RegisterServer(ZRpc rpc) { CharacterVaultPlugin.Settings.InitializeServer(); CharacterVaultRejection.RegisterServer(rpc); rpc.Register<ZPackage>("CharacterVault_Hello_v1", (Action<ZRpc, ZPackage>)ReceiveHello); rpc.Register<ZPackage>("CharacterVault_UploadBegin_v1", (Action<ZRpc, ZPackage>)ReceiveUploadBegin); rpc.Register<ZPackage>("CharacterVault_UploadChunk_v1", (Action<ZRpc, ZPackage>)ReceiveUploadChunk); rpc.Register<ZPackage>("CharacterVault_UploadComplete_v1", (Action<ZRpc, ZPackage>)ReceiveUploadComplete); } private void RegisterClient(ZRpc rpc) { ResetClientState(); CharacterVaultPlugin.DisconnectCoordinator?.RecordConnectionStarted(); CharacterVaultRejection.RegisterClient(rpc); rpc.Register<ZPackage>("CharacterVault_Admission_v1", (Action<ZRpc, ZPackage>)ReceiveAdmission); rpc.Register<ZPackage>("CharacterVault_DownloadBegin_v1", (Action<ZRpc, ZPackage>)ReceiveDownloadBegin); rpc.Register<ZPackage>("CharacterVault_DownloadChunk_v1", (Action<ZRpc, ZPackage>)ReceiveDownloadChunk); rpc.Register<ZPackage>("CharacterVault_DownloadComplete_v1", (Action<ZRpc, ZPackage>)ReceiveDownloadComplete); rpc.Register<string>("CharacterVault_SaveRequest_v1", (Action<ZRpc, string>)ReceiveSaveRequest); rpc.Register<string>("CharacterVault_SaveAck_v1", (Action<ZRpc, string>)ReceiveSaveAck); rpc.Register<string>("CharacterVault_CommitAck_v1", (Action<ZRpc, string>)ReceiveCommitAck); } private void ReceiveHello(ZRpc rpc, ZPackage package) { long characterId = package.ReadLong(); string name = package.ReadString(); bool newCharacter = package.ReadBool(); string hostName = rpc.GetSocket().GetHostName(); _sessions[rpc] = new VaultSession(hostName, characterId, name, newCharacter); } private bool AdmitEnrollment(ZRpc rpc, VaultSession session) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown bool allowMultipleCharacters = CharacterVaultPlugin.Settings.AllowMultipleCharacters; CharacterAdmission characterAdmission = _admission.Decide(hasStoredProfile: false, session.AccountId, session.NewCharacter, allowMultipleCharacters, enrollmentAvailable: true); if (characterAdmission == CharacterAdmission.NewEnrollment && !ReserveEnrollment(rpc, session)) { characterAdmission = CharacterAdmission.RejectConcurrentEnrollment; } if (characterAdmission != CharacterAdmission.NewEnrollment) { _sessions.Remove(rpc); Reject(rpc, CharacterAdmissionMessages.ForRejection(characterAdmission, _storage.GetProfileNames(session.AccountId))); return false; } session.Enrolling = true; ZPackage val = new ZPackage(); val.Write(session.CharacterId); val.Write(CharacterVaultPlugin.Settings.StartingItems.Count); foreach (StartingItem startingItem in CharacterVaultPlugin.Settings.StartingItems) { val.Write(startingItem.Prefab); val.Write(startingItem.Quantity); } rpc.Invoke("CharacterVault_Admission_v1", new object[1] { val }); return true; } private void SendDownload(ZRpc rpc, VaultSession session, byte[] data) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown string text = Guid.NewGuid().ToString("N"); string hash = VaultStorage.Hash(data); rpc.Invoke("CharacterVault_DownloadBegin_v1", new object[1] { ProfileTransferProtocol.Begin(text, data.Length, hash) }); for (int i = 0; i < data.Length; i += 65536) { rpc.Invoke("CharacterVault_DownloadChunk_v1", new object[1] { ProfileTransferProtocol.Chunk(text, data, i) }); } ZPackage val = new ZPackage(); val.Write(text); rpc.Invoke("CharacterVault_DownloadComplete_v1", new object[1] { val }); } private void ReceiveAdmission(ZRpc rpc, ZPackage package) { long num = package.ReadLong(); if (Game.instance.GetPlayerProfile().GetPlayerID() != num) { throw new InvalidDataException("The server admitted a different character."); } int num2 = package.ReadInt(); List<StartingItem> list = new List<StartingItem>(num2); for (int i = 0; i < num2; i++) { list.Add(new StartingItem(package.ReadString(), package.ReadInt())); } _serverStartingItems = list; _clientLifecycle.BeginEnrollment(); } private void ReceiveDownloadBegin(ZRpc rpc, ZPackage package) { _download = IncomingTransfer.Create(package, 67108864); } private void ReceiveDownloadChunk(ZRpc rpc, ZPackage package) { _download?.Add(package); } private void ReceiveDownloadComplete(ZRpc rpc, ZPackage package) { string transferId = package.ReadString(); byte[] array = _download?.Complete(transferId); _download = null; if (array == null) { throw new InvalidDataException("The authoritative profile transfer was incomplete."); } _pendingProfile = ProfileFile.ReplaceSelected(array); _clientLifecycle.ActivateExisting(); } private void ReceiveSaveRequest(ZRpc rpc, string requestId) { if (_clientLifecycle.IsActive && !string.IsNullOrWhiteSpace(requestId)) { _pendingRequest = requestId; if (!_clientUploadBusy) { Game.instance.SavePlayerProfile(true, false); } } } private void ReceiveSaveAck(ZRpc rpc, string requestId) { _clientUploadBusy = false; CharacterVaultPlugin.SaveStatus?.ShowAccepted(requestId); CharacterVaultPlugin.Log.LogInfo("Server accepted character save request " + requestId + "."); CharacterVaultPlugin.DisconnectCoordinator?.RecordSaveCommitted(requestId); if (_pendingRequest != null) { Game.instance.SavePlayerProfile(true, false); } } private void ReceiveCommitAck(ZRpc rpc, string requestId) { CharacterVaultPlugin.SaveStatus?.ShowCommitted(requestId); CharacterVaultPlugin.Log.LogInfo("Server confirmed durable character save request " + requestId + "."); } private void ResetClientState() { _clientLifecycle.Reset(); _clientUploadBusy = false; _suppressNextClientUpload = false; _pendingRequest = null; _pendingProfile = null; CharacterVaultPlugin.SaveStatus?.Hide(); CharacterVaultPlugin.DisconnectCoordinator?.RecordConnectionLost(); } private IEnumerator SendUpload(ZRpc rpc, PlayerProfile profile, byte[] data, string requestId) { string transferId = Guid.NewGuid().ToString("N"); bool sent = false; try { ZPackage val = ProfileTransferProtocol.Begin(transferId, data.Length, VaultStorage.Hash(data)); val.Write(requestId); val.Write(profile.GetPlayerID()); rpc.Invoke("CharacterVault_UploadBegin_v1", new object[1] { val }); for (int offset = 0; offset < data.Length; offset += 65536) { rpc.Invoke("CharacterVault_UploadChunk_v1", new object[1] { ProfileTransferProtocol.Chunk(transferId, data, offset) }); yield return null; } ZPackage val2 = new ZPackage(); val2.Write(transferId); rpc.Invoke("CharacterVault_UploadComplete_v1", new object[1] { val2 }); sent = true; } finally { ProfileTransferService profileTransferService = this; if (!sent) { ZNet instance = ZNet.instance; if (((instance != null) ? instance.GetServerRPC() : null) == rpc) { profileTransferService._clientUploadBusy = false; CharacterVaultPlugin.Log.LogWarning("Character save upload " + requestId + " was interrupted before completion."); } } } } private void ReceiveUploadBegin(ZRpc rpc, ZPackage package) { if (TryGetVerifiedSession(rpc, out var session)) { IncomingTransfer incomingTransfer = IncomingTransfer.Create(package, 67108864); incomingTransfer.RequestId = package.ReadString(); if (package.ReadLong() != session.CharacterId) { throw new InvalidDataException("A peer attempted to save a different character."); } _uploads[rpc] = incomingTransfer; } } private void ReceiveUploadChunk(ZRpc rpc, ZPackage package) { if (_uploads.TryGetValue(rpc, out var value)) { value.Add(package); } } private void ReceiveUploadComplete(ZRpc rpc, ZPackage package) { string transferId = package.ReadString(); if (!_uploads.TryGetValue(rpc, out var value) || !TryGetVerifiedSession(rpc, out var session)) { return; } _uploads.Remove(rpc); byte[] data = value.Complete(transferId); if (ValidateProfile(rpc, session, data)) { _finalSaveMonitor.RecordSaveReceived(rpc, value.RequestId); if (!SaveAcknowledgementPolicy.CanAcknowledge(session.State)) { CharacterVaultPlugin.Log.LogWarning("Rejected character save " + value.RequestId + " for " + session.Name + ": the player is not permitted to save on this server."); } else if (session.Enrolling) { _storage.Commit(session.AccountId, session.Name, data); ConfirmCommit(rpc, session, value.RequestId); } else { ConfirmReceipt(rpc, session, value.RequestId); QueueCommit(rpc, session, value.RequestId, data); } } } private void ConfirmReceipt(ZRpc rpc, VaultSession session, string requestId) { rpc.Invoke("CharacterVault_SaveAck_v1", new object[1] { requestId }); CharacterVaultPlugin.Log.LogMessage("Accepted character profile for " + session.Name + " for request " + requestId + "; durable commit queued."); } private void QueueCommit(ZRpc rpc, VaultSession session, string requestId, byte[] data) { _commits.Enqueue(new PendingCommit(rpc, session, requestId, data)); } private void ConfirmBackgroundCommit(PendingCommit commit) { CharacterVaultPlugin.Log.LogMessage("Committed character profile for " + commit.Session.Name + " for request " + commit.RequestId + "."); if (_sessions.TryGetValue(commit.Rpc, out var value) && value == commit.Session) { commit.Rpc.Invoke("CharacterVault_CommitAck_v1", new object[1] { commit.RequestId }); CharacterVaultPlugin.Coordinator?.RecordSaveCommitted(commit.Rpc, commit.RequestId); CharacterVaultPlugin.ServerDisconnects?.RecordCommitted(commit.Rpc, commit.RequestId); } } private void ConfirmCommit(ZRpc rpc, VaultSession session, string requestId) { if (_sessions.TryGetValue(rpc, out var value) && value == session) { session.Enrolling = false; ReleaseEnrollment(rpc); rpc.Invoke("CharacterVault_SaveAck_v1", new object[1] { requestId }); rpc.Invoke("CharacterVault_CommitAck_v1", new object[1] { requestId }); CharacterVaultPlugin.Log.LogMessage("Saved character profile for " + session.Name + " for request " + requestId + "."); CharacterVaultPlugin.Coordinator?.RecordSaveCommitted(rpc, requestId); CharacterVaultPlugin.ServerDisconnects?.RecordCommitted(rpc, requestId); } } private bool TryGetVerifiedSession(ZRpc rpc, out VaultSession session) { session = null; if (_sessions.TryGetValue(rpc, out session)) { return session.State.CanSave; } return false; } private bool ValidateProfile(ZRpc rpc, VaultSession session, byte[] data) { try { ProfileUploadValidator.Validate(session, data); return true; } catch (InvalidDataException ex) { CharacterVaultPlugin.Log.LogError($"Character profile validation failed for {session.Name}: {ex}"); _sessions.Remove(rpc); _uploads.Remove(rpc); ReleaseEnrollment(rpc); CharacterVaultRejection.Reject(rpc, "Your character data could not be validated. Please restart the game and try again.", ex.Message); return false; } } private bool ReserveEnrollment(ZRpc rpc, VaultSession session) { if (CharacterVaultPlugin.Settings.AllowMultipleCharacters) { return true; } if (_enrollments.TryGetValue(session.AccountId, out var value) && value != rpc) { return false; } _enrollments[session.AccountId] = rpc; return true; } private void ReleaseEnrollment(ZRpc rpc) { string key = _enrollments.FirstOrDefault((KeyValuePair<string, ZRpc> pair) => pair.Value == rpc).Key; if (key != null) { _enrollments.Remove(key); } } private static void Reject(ZRpc rpc, string message) { CharacterVaultRejection.Reject(rpc, message); } private static bool IsReady(ZNetPeer peer) { if (peer?.m_rpc != null && peer.IsReady()) { ISocket socket = peer.m_socket; if (socket == null) { return false; } return socket.IsConnected(); } return false; } private static bool CanSave(Dictionary<ZRpc, VaultSession> sessions, ZRpc rpc) { if (sessions.TryGetValue(rpc, out var value)) { return value.State.CanSave; } return false; } private static GameObject FindItem(string name) { return ((IEnumerable<GameObject>)ObjectDB.instance?.m_items).FirstOrDefault((Func<GameObject, bool>)((GameObject item) => string.Equals(((Object)item).name, name, StringComparison.OrdinalIgnoreCase))); } } internal static class ProfileUploadValidator { internal static void Validate(VaultSession session, byte[] data) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown string text = "character_vault_validation_" + Guid.NewGuid().ToString("N"); FileSource localSource = SaveApiCompatibility.LocalSource; string characterPath = SaveApiCompatibility.GetCharacterPath(localSource, text); Directory.CreateDirectory(Path.GetDirectoryName(characterPath)); File.WriteAllBytes(characterPath, data); try { PlayerProfile val = new PlayerProfile(text, localSource); if (!val.Load() || val.GetPlayerID() != session.CharacterId || !string.Equals(val.GetName(), session.Name, StringComparison.Ordinal)) { throw new InvalidDataException("The uploaded profile identity is invalid."); } } finally { File.Delete(characterPath); SaveApiCompatibility.InvalidateCharacterCache(); } } } internal static class BackupRetention { private sealed class BackupFile { internal string Path { get; } internal DateTime Timestamp { get; } internal BackupFile(string path, DateTime timestamp) { Path = path; Timestamp = timestamp; } } private const int RecentBackupCount = 5; private const int DailyBackupCount = 10; private const string TimestampFormat = "yyyyMMdd'T'HHmmssfffffff'Z'"; internal static IReadOnlyList<string> Apply(string directory, string profileName) { List<BackupFile> source = (from backup in FindBackups(directory, profileName) orderby backup.Timestamp descending select backup).ToList(); HashSet<string> retained = new HashSet<string>(from backup in source.Take(5) select backup.Path, StringComparer.Ordinal); DateTime dailyBoundary = (from backup in source.Take(5) select backup.Timestamp.Date).DefaultIfEmpty(DateTime.MinValue).Last(); IEnumerable<BackupFile> source2 = from @group in (from backup in source.Skip(5) where backup.Timestamp.Date < dailyBoundary group backup by backup.Timestamp.Date into @group orderby @group.Key descending select @group).Take(10) select @group.OrderBy((BackupFile backup) => backup.Timestamp).First(); retained.UnionWith(source2.Select((BackupFile backup) => backup.Path)); List<string> list = new List<string>(); foreach (BackupFile item in source.Where((BackupFile backup) => !retained.Contains(backup.Path))) { File.Delete(item.Path); list.Add(Path.GetFileName(item.Path)); } return list; } private static IEnumerable<BackupFile> FindBackups(string directory, string profileName) { string prefix = profileName + "_"; string[] files = Directory.GetFiles(directory, "*.fch"); foreach (string path in files) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); if (fileNameWithoutExtension.StartsWith(prefix, StringComparison.Ordinal) && DateTime.TryParseExact(fileNameWithoutExtension.Substring(prefix.Length), "yyyyMMdd'T'HHmmssfffffff'Z'", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { yield return new BackupFile(path, result); } } } } internal sealed class VaultStorage : ICharacterProfileCatalog { private const string BackupDirectory = "backups"; bool ICharacterProfileCatalog.HasProfile(string accountId) { return HasProfile(accountId); } internal bool TryRead(string accountId, string name, out byte[] data) { string path = ProfilePath(accountId, name); if (!File.Exists(path)) { path = FindProfilePath(accountId, name); } data = (File.Exists(path) ? File.ReadAllBytes(path) : null); return data != null; } private static string FindProfilePath(string accountId, string name) { string prefix = SafeSegment(accountId) + "_"; string path = StorageRoot(); if (!Directory.Exists(path)) { return string.Empty; } return Directory.GetFiles(path, prefix + "*.fch", SearchOption.TopDirectoryOnly).FirstOrDefault((string path2) => string.Equals(Path.GetFileNameWithoutExtension(path2).Substring(prefix.Length), name, StringComparison.OrdinalIgnoreCase)) ?? string.Empty; } internal bool HasProfile(string accountId) { return GetProfileNames(accountId).Count > 0; } internal IReadOnlyList<string> GetProfileNames(string accountId) { string prefix = SafeSegment(accountId) + "_"; string path = StorageRoot(); if (!Directory.Exists(path)) { return Array.Empty<string>(); } return (from fileName in Directory.GetFiles(path, prefix + "*.fch", SearchOption.TopDirectoryOnly).Select(Path.GetFileNameWithoutExtension) where fileName.StartsWith(prefix, StringComparison.Ordinal) select fileName.Substring(prefix.Length) into name where !string.IsNullOrWhiteSpace(name) select name).Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string name) => name, StringComparer.Ordinal).ToArray(); } internal void Commit(string accountId, string name, byte[] data) { Directory.CreateDirectory(StorageRoot()); string text = ProfilePath(accountId, name); string text2 = text + ".new"; WriteDurably(text2, data); PreserveBackup(data, Path.GetFileNameWithoutExtension(text)); Replace(text2, text); } private void PreserveBackup(byte[] data, string profileName) { string text = Path.Combine(StorageRoot(), "backups"); Directory.CreateDirectory(text); string text2 = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmssfffffff'Z'", CultureInfo.InvariantCulture); WriteDurably(Path.Combine(text, profileName + "_" + text2 + ".fch"), data); foreach (string item in BackupRetention.Apply(text, profileName)) { CharacterVaultPlugin.Log.LogInfo("Deleted expired character backup " + item + " for profile " + profileName + "."); } } private static string ProfileFileName(string accountId, string name) { return SafeSegment(accountId) + "_" + SafeSegment(name) + ".fch"; } internal static string SafeSegment(string value) { return new string(value.Select((char character) => (!char.IsControl(character) && !Enumerable.Contains("<>:\"/\\|?*", character)) ? character : '_').ToArray()); } private string ProfilePath(string accountId, string name) { return Path.Combine(StorageRoot(), ProfileFileName(accountId, name)); } private static string StorageRoot() { return Path.Combine(Utils.GetSaveDataPath((FileSource)2), "characters_local"); } private static void WriteDurably(string path, byte[] data) { using FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough); fileStream.Write(data, 0, data.Length); fileStream.Flush(flushToDisk: true); } private static void Replace(string source, string destination) { if (File.Exists(destination)) { File.Replace(source, destination, null); } else { File.Move(source, destination); } } internal static string Hash(byte[] data) { using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(data)).Replace("-", string.Empty); } } internal sealed class GracefulShutdownCoordinator : IDisposable { private const string ExitFilePath = "character_vault.drp"; private const int MaximumConcurrentSaves = 4; private const int ShutdownTimeoutSeconds = 90; private const int ClientDisconnectGraceSeconds = 2; private readonly HashSet<ZNetPeer> _pendingPeers = new HashSet<ZNetPeer>(); private readonly HashSet<ZNetPeer> _requestedPeers = new HashSet<ZNetPeer>(); private readonly HashSet<ZRpc> _shutdownPeerRpcs = new HashSet<ZRpc>(); private readonly Queue<ZNetPeer> _queuedPeers = new Queue<ZNetPeer>(); private readonly FileSystemWatcher _exitFileWatcher; private readonly SynchronizationContext _unityContext; private Timer _timeoutTimer; private Timer _disconnectTimer; 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 arg) { CharacterVaultPlugin.Log.LogError(string.Format("Could not watch {0}; shutdown requests cannot be detected: {1}", "character_vault.drp", arg)); } } 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 arg) { CharacterVaultPlugin.Log.LogError(string.Format("Could not process {0}: {1}", "character_vault.drp", arg)); } } internal void RecordSaveCommitted(ZRpc peerRpc, string committedRequestId) { if (_requestId == null || committedRequestId != _requestId) { return; } ZNetPeer val = ((IEnumerable<ZNetPeer>)_pendingPeers).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_rpc == peerRpc)); if (val != null && _pendingPeers.Remove(val)) { _requestedPeers.Remove(val); CharacterVaultPlugin.Log.LogMessage($"Confirmed graceful character save for {val.m_playerName} ({_pendingPeers.Count} remaining)."); RequestNextProfiles(); if (_pendingPeers.Count == 0) { Complete(); } } } 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); _shutdownPeerRpcs.Add(item.m_rpc); _queuedPeers.Enqueue(item); } CharacterVaultPlugin.Log.LogMessage($"Shutdown requested; saving {_pendingPeers.Count} connected character(s)."); RequestNextProfiles(); CompleteIfFinished(); } internal void ProcessPendingExitRequest() { if (_exitRequestPending) { ProcessExitRequest(); } } internal bool TryRequestShutdown() { if (_shutdownCommitted || _requestId != null || !CanCoordinateShutdown()) { return false; } Start(ZNet.instance); return true; } 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); CharacterVaultPlugin.Transfers.RequestSave(val, _requestId); } } public void Dispose() { Interlocked.Exchange(ref _disposed, 1); _exitFileWatcher?.Dispose(); _timeoutTimer?.Dispose(); _disconnectTimer?.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 void Complete() { CharacterVaultPlugin.Log.LogMessage("All connected character profiles were written to disk; requesting normal client disconnection."); RequestClientDisconnects(); } 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}."); CharacterVaultPlugin.Log.LogWarning("Requesting normal client disconnection after the character save timeout."); RequestClientDisconnects(); } private void RequestClientDisconnects() { ZNetPeer[] array = ConnectedShutdownPeers(); ZNetPeer[] array2 = array; for (int i = 0; i < array2.Length; i++) { array2[i].m_rpc.Invoke("Disconnect", Array.Empty<object>()); } if (array.Length == 0) { ContinueVanillaShutdown(); return; } CharacterVaultPlugin.Log.LogMessage($"Requested normal disconnection for {array.Length} client(s); " + $"waiting {2} seconds before the server fallback."); _disconnectTimer = new Timer(OnDisconnectGraceElapsed, null, TimeSpan.FromSeconds(2.0), Timeout.InfiniteTimeSpan); } private ZNetPeer[] ConnectedShutdownPeers() { ZNet instance = ZNet.instance; IEnumerable<ZNetPeer> enumerable = ((instance != null) ? instance.GetPeers() : null); return (enumerable ?? Enumerable.Empty<ZNetPeer>()).Where((ZNetPeer peer) => peer?.m_rpc != null && _shutdownPeerRpcs.Contains(peer.m_rpc)).ToArray(); } private void OnDisconnectGraceElapsed(object state) { if (Volatile.Read(in _disposed) == 0) { _unityContext.Post(delegate { CompleteClientDisconnects(); }, null); } } private void CompleteClientDisconnects() { ZNetPeer[] array = ConnectedShutdownPeers(); ZNetPeer[] array2 = array; foreach (ZNetPeer val in array2) { ZNet.instance.Disconnect(val); } CharacterVaultPlugin.Log.LogMessage((array.Length == 0) ? "All clients disconnected normally; continuing the vanilla shutdown." : $"Closed {array.Length} remaining client connection(s); continuing the vanilla shutdown."); ContinueVanillaShutdown(); } private void ContinueVanillaShutdown() { _timeoutTimer?.Dispose(); _timeoutTimer = null; _disconnectTimer?.Dispose(); _disconnectTimer = null; _pendingPeers.Clear(); _requestedPeers.Clear(); _shutdownPeerRpcs.Clear(); _queuedPeers.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; } } public static class GracefulShutdownApi { public static bool TryRequest() { return CharacterVaultPlugin.Coordinator?.TryRequestShutdown() ?? false; } } internal interface IKickSaveRequest { KickSaveRequestResult Request(); } internal sealed class KickSaveRequestResult { internal string RequestId { get; } internal bool Started { get; } internal KickSaveRequestResult(bool started, string requestId = "") { Started = started; RequestId = requestId; } } internal enum KickSaveEligibility { Unmanaged, Rejected, SaveRequired } internal enum KickAction { Allow, AllowWithoutSave, WaitForPendingSave, RequestSave } internal static class KickSavePolicy { internal static KickAction Decide(bool validServerPeer, bool saveAuthorized, bool savePending, KickSaveEligibility eligibility) { if (!validServerPeer || saveAuthorized) { return KickAction.Allow; } if (eligibility == KickSaveEligibility.Rejected) { return KickAction.AllowWithoutSave; } if (savePending) { return KickAction.WaitForPendingSave; } if (eligibility != KickSaveEligibility.SaveRequired) { return KickAction.AllowWithoutSave; } return KickAction.RequestSave; } } internal static class KickSaveRequestExecutor { internal static KickSaveRequestResult Execute(KickAction action, IKickSaveRequest request) { if (action != KickAction.RequestSave) { return new KickSaveRequestResult(started: false); } return request.Request(); } } internal sealed class VoluntaryDisconnectCoordinator : IDisposable { private const float ConfirmationTimeoutSeconds = 10f; private bool _allowApplicationQuit; private bool _allowLogout; private Game _game; private bool _logoutSave; private bool _logoutStartScene; private string _requestId; private VoluntaryExitKind _exitKind; private bool _playerEnteredWorld; internal bool HasPendingSave => _requestId != null; internal bool AllowLogout(Game game, bool save, bool changeToStartScene) { if (_allowLogout) { _allowLogout = false; CharacterVaultPlugin.Log.LogInfo("Allowing voluntary logout after the final character save was accepted."); return true; } if (!save || !Start(VoluntaryExitKind.Logout, game, save, changeToStartScene)) { return true; } return false; } internal void RecordSaveCommitted(string requestId) { if (!(requestId != _requestId)) { CharacterVaultPlugin.Log.LogMessage("Final voluntary disconnect save " + requestId + " accepted."); CharacterVaultPlugin.Transfers.SuppressRedundantDisconnectUpload(); CompletePendingExit("after the confirmed save"); } } internal void RecordConnectionStarted() { _playerEnteredWorld = false; ClearPendingRequest(); } internal void RecordPlayerSpawned() { _playerEnteredWorld = true; CharacterVaultPlugin.Log.LogInfo("CharacterVault final-save protection armed after the local player spawned."); } internal void RecordConnectionLost() { if (_requestId != null) { CharacterVaultPlugin.Log.LogWarning("Connection was lost while final save " + _requestId + " was pending; confirmation is impossible."); ClearPendingRequest(); _playerEnteredWorld = false; } } internal bool AllowMenuQuit() { if (_allowApplicationQuit) { return true; } bool num = Start(VoluntaryExitKind.ApplicationQuit, Game.instance, save: true, startScene: false); if (num) { CharacterVaultPlugin.Log.LogMessage("Intercepted the in-game Quit action; waiting for the final save acceptance."); } return !num; } public void Dispose() { ClearPendingRequest(); } private bool Start(VoluntaryExitKind kind, Game game, bool save, bool startScene) { string requestId = "disconnect-" + Guid.NewGuid().ToString("N"); IVoluntaryExitSaveRequest request = new VoluntaryExitSaveRequest(() => CharacterVaultPlugin.Transfers?.BeginFinalDisconnectSave(requestId) ?? false); VoluntaryExitSaveAction voluntaryExitSaveAction = VoluntaryExitSavePolicy.Start(_playerEnteredWorld, _requestId != null, request); if (voluntaryExitSaveAction != VoluntaryExitSaveAction.WaitForNewSave) { return voluntaryExitSaveAction == VoluntaryExitSaveAction.WaitForPendingSave; } _requestId = requestId; _exitKind = kind; _game = game; _logoutSave = save; _logoutStartScene = startScene; CharacterVaultPlugin.Log.LogMessage("Delayed voluntary " + Describe(kind) + " until final save " + requestId + " is committed."); CharacterVaultPlugin.Instance.Run(WaitForConfirmation(requestId)); return true; } private IEnumerator WaitForConfirmation(string requestId) { float deadline = Time.realtimeSinceStartup + 10f; while (_requestId == requestId && Time.realtimeSinceStartup < deadline) { yield return null; } if (!(_requestId != requestId)) { CharacterVaultPlugin.Log.LogError("Allowing voluntary " + Describe(_exitKind) + " because final save " + requestId + " " + $"was not confirmed within {10f:0} seconds."); CompletePendingExit("after the confirmation timeout"); } } private void CompletePendingExit(string reason) { VoluntaryExitKind exitKind = _exitKind; Game game = _game; bool logoutSave = _logoutSave; bool logoutStartScene = _logoutStartScene; ClearPendingRequest(); if (exitKind == VoluntaryExitKind.ApplicationQuit) { _allowApplicationQuit = true; CharacterVaultPlugin.Log.LogInfo("Allowing application quit " + reason + "."); Application.Quit(); } else { _allowLogout = true; CharacterVaultPlugin.Log.LogInfo("Allowing logout " + reason + "."); game.Logout(logoutSave, logoutStartScene); } } private void ClearPendingRequest() { _requestId = null; _game = null; } private static string Describe(VoluntaryExitKind kind) { if (kind != VoluntaryExitKind.ApplicationQuit) { return "logout"; } return "application quit"; } } internal enum VoluntaryExitKind { Logout, ApplicationQuit } internal sealed class VoluntaryExitSaveRequest : IVoluntaryExitSaveRequest { private readonly Func<bool> _request; internal VoluntaryExitSaveRequest(Func<bool> request) { _request = request; } public bool Request() { return _request(); } } internal interface IVoluntaryExitSaveRequest { bool Request(); } internal enum VoluntaryExitSaveAction { PassThrough, WaitForPendingSave, WaitForNewSave } internal static class VoluntaryExitSavePolicy { internal static VoluntaryExitSaveAction Start(bool playerEnteredWorld, bool savePending, IVoluntaryExitSaveRequest request) { if (!playerEnteredWorld) { return VoluntaryExitSaveAction.PassThrough; } if (savePending) { return VoluntaryExitSaveAction.WaitForPendingSave; } if (!request.Request()) { return VoluntaryExitSaveAction.PassThrough; } return VoluntaryExitSaveAction.WaitForNewSave; } } internal static class WorldSavePolicy { internal static void Handle(bool isServer, Action requestCharacterCheckpoint) { if (isServer) { requestCharacterCheckpoint(); } } } internal static class StartingItemGrantPolicy { internal static bool ApplyEnrollment<TItem>(ClientSaveLifecycle lifecycle, bool isLocalPlayer, IEnumerable<StartingItem> startingItems, Func<string, TItem> findItem, Func<TItem, int, bool> addItem, Action saveProfile, Action<StartingItem> reportFailure) where TItem : class { if (!lifecycle.RecordSpawn(isLocalPlayer)) { return false; } foreach (StartingItem startingItem in startingItems) { if (!Grant(startingItem.Prefab, startingItem.Quantity, findItem, addItem)) { reportFailure(startingItem); } } saveProfile(); return true; } internal static bool Grant<TItem>(string prefabName, int quantity, Func<string, TItem> findItem, Func<TItem, int, bool> addItem) where TItem : class { TItem val = findItem(prefabName); if (val != null) { return addItem(val, quantity); } return false; } } internal sealed class StartingItem { internal string Prefab { get; } internal int Quantity { get; } internal StartingItem(string prefab, int quantity) { Prefab = prefab; Quantity = quantity; } } internal sealed class ServerDisconnectSaveCoordinator : IDisposable { private const float ConfirmationTimeoutSeconds = 30f; private readonly Dictionary<string, PendingServerSave> _pending = new Dictionary<string, PendingServerSave>(StringComparer.Ordinal); private readonly HashSet<ZRpc> _authorizedDisconnects = new HashSet<ZRpc>(); internal bool AllowKick(ZNet network, ZNetPeer peer) { ZNet obj = network; int num; int num2; if (obj != null && obj.IsServer()) { num = ((peer?.m_rpc != null) ? 1 : 0); if (num != 0) { num2 = (_authorizedDisconnects.Remove(peer.m_rpc) ? 1 : 0); goto IL_0063; } } else { num = 0; } num2 = 0; goto IL_0063; IL_0063: bool flag = (byte)num2 != 0; bool savePending = num != 0 && HasPendingRequest(peer.m_rpc); KickSaveEligibility eligibility = ((num != 0) ? (CharacterVaultPlugin.Transfers?.GetKickSaveEligibility(peer) ?? KickSaveEligibility.Unmanaged) : KickSaveEligibility.Unmanaged); KickAction action = KickSavePolicy.Decide((byte)num != 0, flag, savePending, eligibility); if (TryResolveWithoutSave(action, flag, peer, out var allow)) { return allow; } string requestId; IKickSaveRequest request = new KickSaveRequestOperation(() => new KickSaveRequestResult(TryRequest(peer, "server kick", delegate(string requestId2, bool saved) { CompleteKick(network, peer, requestId2, saved); }, out requestId), requestId)); KickSaveRequestResult kickSaveRequestResult = KickSaveRequestExecutor.Execute(action, request); if (!kickSaveRequestResult.Started) { CharacterVaultPlugin.Log.LogWarning("A final save could not be requested for " + peer.m_playerName + "; allowing the kick without it."); return true; } CharacterVaultPlugin.Log.LogMessage("Delayed kick for " + peer.m_playerName + " until final save " + kickSaveRequestResult.RequestId + " is committed."); return false; } private static bool TryResolveWithoutSave(KickAction action, bool authorized, ZNetPeer peer, out bool allow) { allow = action == KickAction.Allow || action == KickAction.AllowWithoutSave; if (action == KickAction.Allow && authorized) { CharacterVaultPlugin.Log.LogInfo("Allowing kick for " + peer.m_playerName + " after its confirmed final save."); } else { switch (action) { case KickAction.AllowWithoutSave: CharacterVaultPlugin.Log.LogInfo("Allowing kick for " + peer.m_playerName + " without a character save."); break; case KickAction.WaitForPendingSave: CharacterVaultPlugin.Log.LogWarning("Ignored another kick for " + peer.m_playerName + " while its final save is pending."); break; } } return action != KickAction.RequestSave; } internal bool TryRequest(ZNetPeer peer, string reason, Action<string, bool> completed, out string requestId) { requestId = null; if (peer?.m_rpc != null && completed != null) { ZNet instance = ZNet.instance; if (instance != null && instance.IsServer() && peer.IsReady()) { ProfileTransferService transfers = CharacterVaultPlugin.Transfers; if (transfers != null && transfers.CanRequestSave(peer)) { requestId = "server-disconnect-" + Guid.NewGuid().ToString("N"); _pending[requestId] = new PendingServerSave(peer.m_rpc, peer.m_playerName, reason, completed); CharacterVaultPlugin.Log.LogMessage("Requesting final save " + requestId + " for " + peer.m_playerName + " before " + reason + "."); CharacterVaultPlugin.Transfers.RequestSave(peer, requestId); CharacterVaultPlugin.Instance.Run(WaitForConfirmation(requestId)); return true; } } } return false; } internal void RecordCommitted(ZRpc rpc, string requestId) { if (_pending.TryGetValue(requestId, out var value) && value.Rpc == rpc) { _pending.Remove(requestId); _authorizedDisconnects.Add(rpc); CharacterVaultPlugin.Log.LogMessage("Final save " + requestId + " for " + value.PlayerName + " committed; authorizing " + value.Reason + "."); value.Completed(requestId, arg2: true); } } internal void RecordDisconnected(ZRpc rpc) { _authorizedDisconnects.Remove(rpc); foreach (string item in RequestsFor(rpc)) { CompleteFailed(item, "the connection closed before confirmation"); } } internal bool HasPendingSave(ZRpc rpc) { if (rpc != null) { return HasPendingRequest(rpc); } return false; } public void Dispose() { _authorizedDisconnects.Clear(); foreach (string item in new List<string>(_pending.Keys)) { CompleteFailed(item, "CharacterVault unloaded before confirmation"); } } private void CompleteKick(ZNet network, ZNetPeer peer, string requestId, bool saved) { if (!saved) { CharacterVaultPlugin.Log.LogWarning("Final save " + requestId + " for " + peer.m_playerName + " was not confirmed; proceeding with the kick."); } else { CharacterVaultPlugin.Log.LogMessage("Replaying kick for " + peer.m_playerName + " after save " + requestId + "."); } ZRpc val = peer?.m_rpc; if (val == null || peer.m_socket == null) { return; } _authorizedDisconnects.Add(val); try { network.Kick(peer.m_socket.GetHostName()); } finally { _authorizedDisconnects.Remove(val); } } private bool HasPendingRequest(ZRpc rpc) { foreach (PendingServerSave value in _pending.Values) { if (value.Rpc == rpc) { return true; } } return false; } private IEnumerator WaitForConfirmation(string requestId) { float deadline = Time.realtimeSinceStartup + 30f; while (_pending.ContainsKey(requestId) && Time.realtimeSinceStartup < deadline) { yield return null; } if (_pending.ContainsKey(requestId)) { CompleteFailed(requestId, $"no commit acknowledgement arrived within {30f:0} seconds"); } } private void CompleteFailed(string requestId, string reason) { if (_pending.TryGetValue(requestId, out var value)) { _pending.Remove(requestId); CharacterVaultPlugin.Log.LogWarning("Final save " + requestId + " for " + value.PlayerName + " was abandoned: " + reason + "; " + value.Reason + " will continue."); value.Completed(requestId, arg2: false); } } private List<string> RequestsFor(ZRpc rpc) { List<string> list = new List<string>(); foreach (KeyValuePair<string, PendingServerSave> item in _pending) { if (item.Value.Rpc == rpc) { list.Add(item.Key); } } return list; } } internal sealed class KickSaveRequestOperation : IKickSaveRequest { private readonly Func<KickSaveRequestResult> _request; internal KickSaveRequestOperation(Func<KickSaveRequestResult> request) { _request = request; } public KickSaveRequestResult Request() { return _request(); } } internal sealed class PendingServerSave { internal Action<string, bool> Completed { get; } internal string PlayerName { get; } internal string Reason { get; } internal ZRpc Rpc { get; } internal PendingServerSave(ZRpc rpc, string playerName, string reason, Action<string, bool> completed) { Rpc = rpc; PlayerName = playerName; Reason = reason; Completed = completed; } } public static class ServerDisconnectApi { public static bool TrySaveBeforeDisconnect(ZRpc rpc, string reason, Action<bool> completed) { ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate?.m_rpc != null && candidate.m_rpc == rpc)) : null); if (val == null || completed == null) { return false; } string requestId; return CharacterVaultPlugin.ServerDisconnects?.TryRequest(val, reason, delegate(string _, bool saved) { completed(saved); }, out requestId) ?? false; } } [HarmonyPatch(typeof(ZNet), "SaveWorldAndPlayerProfiles")] internal static class CharacterVaultManualSavePatch { private static void Prefix() { CharacterVaultPlugin.Transfers?.SaveManualClientProfile(); } } [HarmonyPatch(typeof(Minimap), "Start")] internal static class CharacterVaultSaveStatusPatch { private static void Postfix(Minimap __instance) { CharacterVaultPlugin.SaveStatus?.Attach(__instance); } } [HarmonyPatch(typeof(ZNet), "Start")] internal static class PendingExitRequestPatch { private static void Postfix(ZNet __instance) { if (__instance.IsServer()) { CharacterVaultPlugin.Settings?.InitializeServer(); } CharacterVaultPlugin.Coordinator?.ProcessPendingExitRequest(); } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class CharacterVaultConnectionPatch { private static void Postfix(ZNet __instance, ZNetPeer peer) { CharacterVaultPlugin.Transfers?.Register(__instance, peer); } } [HarmonyPatch(typeof(ZNet), "SendPeerInfo")] internal static class CharacterVaultHelloPatch { private static void Prefix(ZRpc rpc, bool __runOriginal) { if (__runOriginal) { ZNet instance = ZNet.instance; if (instance != null && !instance.IsServer()) { CharacterVaultPlugin.Transfers?.SendHello(rpc); } } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class CharacterVaultAdmissionBarrierPatch { private static bool Prefix(ZRpc rpc, bool __runOriginal) { if (__runOriginal) { ZNet instance = ZNet.instance; if (instance != null && instance.IsServer()) { return CharacterVaultPlugin.Transfers?.Approve(rpc) ?? false; } } return true; } } [HarmonyPatch(typeof(ZNet), "IsAllowed")] internal static class CharacterVaultPermittedListReasonPatch { private static void Postfix(string hostName, string playerName, SyncedList ___m_bannedList, SyncedList ___m_permittedList, Platform ___m_steamPlatform, bool __result) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) CharacterVaultPlugin.Transfers?.RecordPermission(hostName, __result); if (!__result && !IsListed(___m_bannedList, hostName, ___m_steamPlatform) && !___m_bannedList.Contains(playerName) && ___m_permittedList.Count() != 0 && !IsListed(___m_permittedList, hostName, ___m_steamPlatform)) { CharacterVaultRejection.RecordPermittedListRejection(hostName); } } private unsafe static bool IsListed(SyncedList list, string value, Platform steamPlatform) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0028: 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_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) PlatformUserID val = default(PlatformUserID); if (!PlatformUserID.TryParse(value, ref val)) { ((PlatformUserID)(ref val))..ctor(steamPlatform, value); } if (!list.Contains(((object)(*(PlatformUserID*)(&val))/*cast due to .constrained prefix*/).ToString())) { if (val.m_platform == steamPlatform) { return list.Contains(val.m_userID.ToString()); } return false; } return true; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class CharacterVaultPermittedListMessagePatch { private static void Postfix(ZRpc rpc) { ZNet instance = ZNet.instance; if (instance != null && instance.IsServer()) { CharacterVaultRejection.SendPermittedListRejection(rpc); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] internal static class CharacterVaultDisconnectPatch { private static void Prefix(ZNetPeer peer) { ZNet instance = ZNet.instance; bool server = instance != null && instance.IsServer(); string text = CharacterVaultPlugin.Transfers?.DescribeDisconnect(peer, server) ?? "state unavailable"; CharacterVaultPlugin.Log?.LogDebug("Observed peer disconnection before CharacterVault cleanup: " + text + "."); CharacterVaultPlugin.Transfers?.Remove(peer); if (peer?.m_rpc != null) { CharacterVaultRejection.Remove(peer.m_rpc); } } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class CharacterVaultClientNetworkDestroyPatch { private static void Prefix(ZNet __instance) { if (!__instance.IsServer()) { ZNetPeer serverPeer = __instance.GetServerPeer(); string text = CharacterVaultPlugin.Transfers?.DescribeDisconnect(serverPeer, server: false) ?? "state unavailable"; CharacterVaultPlugin.Log?.LogDebug("Observed client network teardown before CharacterVault cleanup: " + text + "."); } } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] internal static class CharacterVaultPublishPlayFabFailurePatch { private static void Prefix() { PlayFabConnectionDiagnostics.PublishBlockingFailure(); } } [HarmonyPatch(typeof(ZNet), "InternalKick", new Type[] { typeof(ZNetPeer) })] internal static class CharacterVaultKickBarrierPatch { private static bool Prefix(ZNet __instance, ZNetPeer peer) { return CharacterVaultPlugin.ServerDisconnects?.AllowKick(__instance, peer) ?? true; } } [HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")] internal static class CharacterVaultProfileSavedPatch { private static void Postfix(PlayerProfile __instance, bool __result) { if (__result) { CharacterVaultPlugin.Transfers?.UploadSavedProfile(__instance); } } } [HarmonyPatch(typeof(Game), "Logout")] internal static class CharacterVaultVoluntaryLogoutPatch { private static bool Prefix(Game __instance, bool save, bool changeToStartScene) { CharacterVaultPlugin.Log.LogDebug($"Game.Logout invoked: save={save}, changeToStartScene={changeToStartScene}, " + $"pendingCharacterSave={CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false}."); return CharacterVaultPlugin.DisconnectCoordinator?.AllowLogout(__instance, save, changeToStartScene) ?? true; } } [HarmonyPatch(typeof(Game), "ContinueLogout")] internal static class CharacterVaultContinueLogoutDiagnosticsPatch { private static void Prefix(bool save, bool shouldExit, bool changeToStartScene) { CharacterVaultPlugin.Log.LogDebug($"Game.ContinueLogout invoked: save={save}, shouldExit={shouldExit}, " + $"changeToStartScene={changeToStartScene}, " + $"pendingCharacterSave={CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false}."); } } [HarmonyPatch(typeof(Game), "OnDestroy")] internal static class CharacterVaultGameDestroyDiagnosticsPatch { private static void Prefix() { CharacterVaultPlugin.Log.LogDebug("Game.OnDestroy invoked: " + $"pendingCharacterSave={CharacterVaultPlugin.DisconnectCoordinator?.HasPendingSave ?? false}."); } } [HarmonyPatch(typeof(Menu), "OnLogout")] internal static class CharacterVaultLogoutButtonDiagnosticsPatch { private static void Prefix() { CharacterVaultPlugin.Log?.LogDebug("The in-ga