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 RunicCharacterVault v1.0.2
RunicCharacterVault.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.Versioning; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using PartyCSharpSDK; using PlayFab; using PlayFab.Party; using RunicCharacterVault.Shared; using Splatform; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Character Vault")] [assembly: AssemblyDescription("Server-authoritative Valheim character storage with durable backups and admission protection")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Character Vault")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2")] [assembly: InternalsVisibleTo("RunicCharacterVault.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.0.2.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunicCharacterVault { 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 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, bool allowExistingCharacters = false) { if (hasStoredProfile) { return CharacterAdmission.ExistingProfile; } if (!createdThisSession && !allowExistingCharacters) { 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 "This server has not imported your character. Ask its administrator to enable AllowExistingCharacters in Character Vault. Your character has not been reset."; } 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 allowExistingCharacters = false) { bool accountHasProfile = !hasStoredProfile && !allowMultipleCharacters && _profiles.HasProfile(accountId); return CharacterAdmissionPolicy.Decide(hasStoredProfile, createdThisSession, allowMultipleCharacters, accountHasProfile, enrollmentAvailable, allowExistingCharacters); } } 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 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."; } 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 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_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: 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 = Object.Instantiate<GameObject>(((Component)minimap.m_biomeNameSmall).gameObject, minimap.m_smallRoot.transform, false); ((Object)val).name = "CharacterVaultSaveStatus"; RectTransform component = val.GetComponent<RectTransform>(); _label = val.GetComponent<TextMeshProUGUI>(); if ((Object)(object)component == (Object)null || (Object)(object)_label == (Object)null || (Object)(object)((TMP_Text)_label).font == (Object)null) { Object.Destroy((Object)(object)val); _label = null; return; } 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(); ((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 static class CharacterVaultArgumentPolicy { private const string MultipleArgument = "--charactervault-allow-multiple-characters"; private const string ItemsArgument = "--charactervault-starting-items"; internal static bool TryResolveAllowMultiple(string[] arguments, out bool result) { result = false; if (!TryReadValue(arguments, "--charactervault-allow-multiple-characters", out var value)) { return false; } if (!bool.TryParse(value, out var result2)) { throw new InvalidOperationException("Command-line switch --charactervault-allow-multiple-characters requires true or false."); } result = result2; return true; } internal static bool TryResolveStartingItems(string[] arguments, out string result) { return TryReadValue(arguments, "--charactervault-starting-items", out result); } 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; } } [BepInPlugin("chazman.RunicCharacterVault", "Runic Character Vault", "1.0.2")] public sealed class CharacterVaultPlugin : RunicPluginBase { internal const string PluginGuid = "chazman.RunicCharacterVault"; internal const string PluginName = "Runic Character Vault"; internal const string PluginVersion = "1.0.2"; internal static ModLog Log { get; private set; } internal static GracefulShutdownCoordinator Coordinator { get; private set; } internal static VoluntaryDisconnectCoordinator DisconnectCoordinator { get; private set; } internal static ServerDisconnectSaveCoordinator ServerDisconnects { get; private set; } internal static CharacterSaveStatusDisplay SaveStatus { get; private set; } internal static CharacterVaultPlugin Instance { get; private set; } internal static CharacterVaultSettings Settings { get; private set; } internal static ProfileTransferService Transfers { get; private set; } internal static bool PlayFabVerboseLogging { get; private set; } private void Awake() { Instance = this; PlayFabVerboseLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "PlayFabVerboseLogging", false, "Enables verbose PlayFab Party logging for local diagnostics.").Value; Log = InitializePlugin("chazman.RunicCharacterVault"); Settings = new CharacterVaultSettings(((BaseUnityPlugin)this).Config); Transfers = new ProfileTransferService(SynchronizationContext.Current); Coordinator = new GracefulShutdownCoordinator(SynchronizationContext.Current); DisconnectCoordinator = new VoluntaryDisconnectCoordinator(); ServerDisconnects = new ServerDisconnectSaveCoordinator(); SaveStatus = new CharacterSaveStatusDisplay(); PlayFabVerboseDiagnostics.Enable(); CharacterVaultLobbyLeftDiagnostics.Register(); Log.LogInfo("Runic Character Vault 1.0.2 is loaded."); } internal void Run(IEnumerator routine) { ((MonoBehaviour)this).StartCoroutine(routine); } internal void QuitNextFrame() { ((MonoBehaviour)this).StartCoroutine(QuitAfterCurrentFrame()); } private void Update() { CharacterVaultRejection.Tick(); Transfers.MonitorFinalSaves(); } private static IEnumerator QuitAfterCurrentFrame() { yield return null; Application.Quit(); } private void OnDestroy() { CharacterVaultLobbyLeftDiagnostics.Unregister(); DisconnectCoordinator?.Dispose(); ServerDisconnects?.Dispose(); Coordinator?.Dispose(); Transfers?.Dispose(); SaveStatus?.Dispose(); CharacterVaultRejection.Clear(); DisconnectCoordinator = null; ServerDisconnects = null; Coordinator = null; Transfers = null; SaveStatus = null; Settings = null; PlayFabVerboseLogging = false; Instance = null; Log?.LogInfo("Runic Character Vault 1.0.2 is unloaded."); ShutdownPlugin(); Log = null; } } internal static class CharacterVaultRejection { [CompilerGenerated] private static class <>O { public static Method <0>__ReceiveAck; public static Action<ZRpc, ZPackage> <1>__ReceiveMessage; } internal const string MessageRpc = "RunicCharacterVault_Rejection_v1"; internal const string AckRpc = "RunicCharacterVault_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_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown object obj = <>O.<0>__ReceiveAck; if (obj == null) { Method val = ReceiveAck; <>O.<0>__ReceiveAck = val; obj = (object)val; } rpc.Register("RunicCharacterVault_RejectionAck_v1", (Method)obj); } internal static void RegisterClient(ZRpc rpc) { ClearClient(); rpc.Register<ZPackage>("RunicCharacterVault_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("RunicCharacterVault_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("RunicCharacterVault", 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("RunicCharacterVault", text, text2); CharacterVaultPlugin.Log.LogWarning("Server rejected the character: " + text + " System detail: " + text2); rpc.Invoke("RunicCharacterVault_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("RunicCharacterVault"); } } internal sealed class CharacterVaultSettings { private bool serverInitialized; private readonly ConfigEntry<bool> allowMultipleCharacters; private readonly ConfigEntry<bool> allowExistingCharacters; private readonly ConfigEntry<string> startingItems; internal bool AllowMultipleCharacters { get; private set; } internal bool AllowExistingCharacters { get; private set; } internal IReadOnlyList<StartingItem> StartingItems { get; private set; } internal CharacterVaultSettings(ConfigFile config) { StartingItems = new List<StartingItem>(); allowExistingCharacters = config.Bind<bool>("Server", "AllowExistingCharacters", true, "Import a previously played character when no vault copy exists. The first upload is trusted; existing vault copies are never replaced by enrollment. Disable for fresh-character-only servers."); allowMultipleCharacters = config.Bind<bool>("Server", "AllowMultipleCharacters", false, "Allow one platform account to enroll more than one character name."); startingItems = config.Bind<string>("Server", "StartingItems", string.Empty, "Optional comma-separated Valheim prefab:quantity pairs for newly enrolled characters."); } internal void InitializeServer() { if (!serverInitialized && ServerRole.IsServer) { string[] commandLineArgs = Environment.GetCommandLineArgs(); AllowExistingCharacters = allowExistingCharacters.Value; AllowMultipleCharacters = (CharacterVaultArgumentPolicy.TryResolveAllowMultiple(commandLineArgs, out var result) ? result : allowMultipleCharacters.Value); string result2; string value = (CharacterVaultArgumentPolicy.TryResolveStartingItems(commandLineArgs, out result2) ? result2 : startingItems.Value); StartingItems = ParseItems(value); serverInitialized = true; CharacterVaultPlugin.Log.LogInfo($"Server allowExistingCharacters={AllowExistingCharacters}, 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(','); foreach (string text in array) { string[] array2 = text.Split(':'); 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; } } 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; } } public static class GracefulShutdownApi { public static bool TryRequest() { return CharacterVaultPlugin.Coordinator?.TryRequestShutdown() ?? false; } } 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; } } 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 static class NewCharacterPolicy { internal static bool HasNeverJoinedAWorld(PlayerProfile profile) { if (profile == null || !profile.m_firstSpawn || profile.m_playerStats == null) { return false; } PlayerStats[] playerStats = profile.m_playerStats; foreach (PlayerStats val in playerStats) { if (val != null && val.m_knownWorlds != null && val.m_knownWorlds.Count > 0) { return false; } } return true; } } internal static class PlayFabAckDiagnostics { private const int HistoryLimit = 32; private static readonly ConditionalWeakTable<InFlightQueue, Queue<string>> Histories = new ConditionalWeakTable<InFlightQueue, Queue<string>>(); internal static void RecordEnqueue(InFlightQueue queue, byte[] payload) { Record(queue, "enqueue " + DescribePayload(payload) + "; " + DescribeQueue(queue)); } internal static void RecordDrop(InFlightQueue queue, byte[] payload) { Record(queue, "drop " + DescribePayload(payload) + "; " + DescribeQueue(queue)); } internal static void RecordReset(InFlightQueue queue) { Record(queue, "reset; " + DescribeQueue(queue)); } internal static void AckReceived(ZPlayFabSocket socket, uint messageId, InFlightQueue queue, bool isClient) { uint num = queue.Head - queue.Tail; uint num2 = messageId - queue.Tail; if (num2 > num) { CharacterVaultPlugin.Log.LogError($"Invalid PlayFab ACK received: ack={messageId}, acknowledged={num2}, " + $"outstanding={num}, {DescribeQueue(queue)}, " + "socket=" + DescribeSocket(socket, isClient) + ". Queue history:\n" + GetHistory(queue) + "\nCall stack:\n" + Environment.StackTrace); } } internal static void RecordIncomingBuffer(ZPlayFabSocket socket, byte[] buffer, bool isClient, bool useCompression, string stage) { if (buffer != null && buffer.Length > 5 && buffer[^1] == 42) { CharacterVaultPlugin.Log.LogWarning("PlayFab ACK-shaped buffer at " + stage + ": " + DescribePayload(buffer) + ", " + $"compression={useCompression}, socket={DescribeSocket(socket, isClient)}, " + "first=" + DescribeBytes(buffer, 0) + ", last=" + DescribeBytes(buffer, Math.Max(0, buffer.Length - 32)) + "."); } } internal static bool IsEarlyCompressedBuffer(byte[] buffer) { if (buffer == null || buffer.Length <= 5) { return false; } int num = (buffer[0] << 8) | buffer[1]; if ((buffer[0] & 0xF) == 8) { return num % 31 == 0; } return false; } internal static void EarlyCompressedBufferQueued(ZPlayFabSocket socket, byte[] buffer, bool isClient) { CharacterVaultPlugin.Log.LogWarning("PlayFab compressed buffer arrived before VersionMatch; queued for selective " + $"decompression: bytes={buffer.Length}, socket={DescribeSocket(socket, isClient)}, " + "first=" + DescribeBytes(buffer, 0) + ", last=" + DescribeBytes(buffer, Math.Max(0, buffer.Length - 32)) + "."); } internal static void AckProcessed(ZPlayFabSocket socket, uint messageId, InFlightQueue queue, bool isClient) { if (!socket.IsConnected()) { CharacterVaultPlugin.Log.LogError($"PlayFab ACK closed the socket: ack={messageId}, {DescribeQueue(queue)}, " + "socket=" + DescribeSocket(socket, isClient) + ". Queue history:\n" + GetHistory(queue)); } } private static void Record(InFlightQueue queue, string entry) { Queue<string> orCreateValue = Histories.GetOrCreateValue(queue); orCreateValue.Enqueue($"{DateTime.UtcNow:O} {entry}"); while (orCreateValue.Count > 32) { orCreateValue.Dequeue(); } } private static string GetHistory(InFlightQueue queue) { if (!Histories.TryGetValue(queue, out var value)) { return "<empty>"; } return string.Join("\n", value.ToArray()); } private static string DescribeQueue(InFlightQueue queue) { return $"queue={RuntimeHelpers.GetHashCode(queue):X8}, head={queue.Head}, " + $"tail={queue.Tail}, bytes={queue.Bytes}, empty={queue.IsEmpty}"; } private static string DescribeSocket(ZPlayFabSocket socket, bool isClient) { return $"object={RuntimeHelpers.GetHashCode(socket):X8}, " + string.Format("side={0}, connected={1}, ", isClient ? "client" : "server", socket.IsConnected()) + "remote=" + PlayFabConnectionDiagnostics.Fingerprint(socket.m_remotePlayerId); } private static string DescribePayload(byte[] payload) { if (payload == null || payload.Length < 5) { return $"bytes={((payload != null) ? payload.Length : 0)}"; } int num = payload.Length - 5; uint num2 = (uint)(payload[num] | (payload[num + 1] << 8) | (payload[num + 2] << 16) | (payload[num + 3] << 24)); return $"id={num2}, type={payload[^1]}, bytes={payload.Length}"; } private static string DescribeBytes(byte[] payload, int offset) { int num = Math.Min(32, payload.Length - offset); if (num <= 0) { return "<empty>"; } return BitConverter.ToString(payload, offset, num); } } [HarmonyPatch(typeof(ZPlayFabSocket), "OnDataMessageReceived")] internal static class CharacterVaultPlayFabRawReceiveDiagnosticsPatch { private static bool Prefix(ZPlayFabSocket __instance, PlayFabPlayer from, byte[] compressedBuffer, bool ___m_isClient, bool ___m_useCompression, PlayFabZLibWorkQueue ___m_zlibWorkQueue) { PlayFabAckDiagnostics.RecordIncomingBuffer(__instance, compressedBuffer, ___m_isClient, ___m_useCompression, "raw receive"); if (___m_useCompression || ((from == null) ? null : from.EntityKey?.Id) != __instance.m_remotePlayerId || !PlayFabAckDiagnostics.IsEarlyCompressedBuffer(compressedBuffer)) { return true; } PlayFabAckDiagnostics.EarlyCompressedBufferQueued(__instance, compressedBuffer, ___m_isClient); ___m_zlibWorkQueue.Decompress(compressedBuffer); return false; } } [HarmonyPatch(typeof(ZPlayFabSocket), "OnDataMessageReceivedCont")] internal static class CharacterVaultPlayFabDecodedReceiveDiagnosticsPatch { private static void Prefix(ZPlayFabSocket __instance, byte[] buffer, bool ___m_isClient, bool ___m_useCompression) { PlayFabAckDiagnostics.RecordIncomingBuffer(__instance, buffer, ___m_isClient, ___m_useCompression, "decoded receive"); } } [HarmonyPatch(typeof(InFlightQueue), "Enqueue")] internal static class CharacterVaultPlayFabQueueEnqueueDiagnosticsPatch { private static void Postfix(InFlightQueue __instance, byte[] payload) { PlayFabAckDiagnostics.RecordEnqueue(__instance, payload); } } [HarmonyPatch(typeof(InFlightQueue), "Drop")] internal static class CharacterVaultPlayFabQueueDropDiagnosticsPatch { private static void Prefix(InFlightQueue __instance, out byte[] __state) { __state = (__instance.IsEmpty ? null : __instance.Peek()); } private static void Postfix(InFlightQueue __instance, byte[] __state) { PlayFabAckDiagnostics.RecordDrop(__instance, __state); } } [HarmonyPatch(typeof(InFlightQueue), "ResetAll")] internal static class CharacterVaultPlayFabQueueResetDiagnosticsPatch { private static void Postfix(InFlightQueue __instance) { PlayFabAckDiagnostics.RecordReset(__instance); } } [HarmonyPatch(typeof(ZPlayFabSocket), "ProcessAck")] internal static class CharacterVaultPlayFabProcessAckDiagnosticsPatch { private static void Prefix(ZPlayFabSocket __instance, uint msgId, InFlightQueue ___m_inFlightQueue, bool ___m_isClient) { PlayFabAckDiagnostics.AckReceived(__instance, msgId, ___m_inFlightQueue, ___m_isClient); } private static void Postfix(ZPlayFabSocket __instance, uint msgId, InFlightQueue ___m_inFlightQueue, bool ___m_isClient) { PlayFabAckDiagnostics.AckProcessed(__instance, msgId, ___m_inFlightQueue, ___m_isClient); } } internal static class PlayFabVerboseDiagnostics { private static bool logged; internal static void Enable() { PlayFabMultiplayerManager val = PlayFabMultiplayerManager.Get(); if ((Object)(object)val == (Object)null) { return; } CharacterVaultLeaveNetworkDiagnosticsPatch.Observe(val); PlayFabEndpointDiagnostics.Observe(val); if (CharacterVaultPlugin.PlayFabVerboseLogging) { val.LogLevel = (LogLevelType)2; if (!logged) { logged = true; CharacterVaultPlugin.Log?.LogDebug("Verbose PlayFab Party logging is enabled."); } } } } internal static class PlayFabConnectionDiagnostics { private sealed class Attempt { internal int Id { get; } internal string Origin { get; } internal bool Connected { get; set; } internal bool Admitted { get; set; } internal bool Failed { get; set; } internal List<Tuple<string, string>> Failures { get; } = new List<Tuple<string, string>>(); internal bool FailuresPublished { get; set; } internal Attempt(int id, string origin) { Id = id; Origin = origin; } internal void AddFailure(string userMessage, string systemMessage) { Tuple<string, string> item = Tuple.Create(userMessage, systemMessage); if (!Failures.Contains(item)) { Failures.Add(item); } } } private static readonly ConditionalWeakTable<ZPlayFabSocket, Attempt> Attempts = new ConditionalWeakTable<ZPlayFabSocket, Attempt>(); private static int nextAttempt; private static string selectedOrigin = "unknown"; private static WeakReference currentClientSocket; internal static void Start(ZPlayFabSocket socket) { CharacterVaultRejection.ClearPendingClientMessage(); Attempt attempt = new Attempt(Interlocked.Increment(ref nextAttempt), selectedOrigin); Attempts.Add(socket, attempt); currentClientSocket = new WeakReference(socket); selectedOrigin = "unknown"; CharacterVaultPlugin.Log?.LogDebug($"PlayFab connection attempt {attempt.Id} started; origin={attempt.Origin}."); } internal static void SelectOrigin(string origin) { selectedOrigin = origin ?? "unknown"; } internal static void SessionFound(ZPlayFabSocket socket, PlayFabMatchmakingServerData server) { if (Attempts.TryGetValue(socket, out var value)) { CharacterVaultPlugin.Log?.LogDebug($"PlayFab connection attempt {value.Id} resolved lobby={Fingerprint(server?.lobbyId)}, " + "network=" + Fingerprint(server?.networkId) + "."); } } internal static void NetworkJoined(ZPlayFabSocket socket, string networkId) { if (Attempts.TryGetValue(socket, out var value)) { CharacterVaultPlugin.Log?.LogDebug($"PlayFab connection attempt {value.Id} joined network={Fingerprint(networkId)}."); } } internal static void Connected(ZPlayFabSocket socket) { if (Attempts.TryGetValue(socket, out var value) && !value.Connected) { value.Connected = true; CharacterVaultPlugin.Log?.LogDebug($"PlayFab connection attempt {value.Id} established the remote transport."); } } internal static void Admitted() { object? obj = currentClientSocket?.Target; ZPlayFabSocket val = (ZPlayFabSocket)((obj is ZPlayFabSocket) ? obj : null); if (val != null && Attempts.TryGetValue(val, out var value) && !value.Admitted) { value.Admitted = true; value.Failures.Clear(); CharacterVaultRejection.ClearPendingClientMessage(); CharacterVaultPlugin.Log?.LogDebug($"PlayFab connection attempt {value.Id} completed the Valheim peer handshake."); } } internal unsafe static void Failed(ZPlayFabSocket socket, ZPLayFabMatchmakingFailReason reason) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) if (Attempts.TryGetValue(socket, out var value)) { value.Failed = true; if (value.Failures.Count == 0) { value.AddFailure(PlayFabConnectionErrorMessages.ForMatchmaking(reason), ((object)(*(ZPLayFabMatchmakingFailReason*)(&reason))/*cast due to .constrained prefix*/).ToString()); } CharacterVaultPlugin.Log?.LogWarning($"PlayFab connection attempt {value.Id} failed while locating the network: {reason}."); } } internal static void Closed(ZPlayFabSocket socket) { //IL_0069: Unknown result type (might be due to invalid IL or missing references) if (Attempts.TryGetValue(socket, out var value)) { string arg = (value.Failed ? "failed" : (value.Admitted ? "admitted" : (value.Connected ? "transport-only" : "incomplete"))); CharacterVaultPlugin.Log?.LogDebug($"PlayFab connection attempt {value.Id} socket closed; outcome={arg}, " + $"status={ZNet.GetConnectionStatus()}."); if (!value.Admitted) { PublishFailures(value); } Attempts.Remove(socket); if (currentClientSocket?.Target == socket) { currentClientSocket = null; } } } internal static void LobbyFailure(string stage, PlayFabError error, string lobbyId = null) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 //IL_00ad: Unknown result type (might be due to invalid IL or missing references) if (error != null && (int)error.Error == 13002) { CharacterVaultPlugin.Log?.LogDebug("PlayFab lobby stage " + stage + " reported an existing membership for lobby=" + Fingerprint(lobbyId) + "; continuing."); } else { RecordCurrentFailure(PlayFabConnectionErrorMessages.ForApi(error), DescribeApiError(stage, error)); CharacterVaultPlugin.Log?.LogWarning("PlayFab lobby stage " + stage + " failed for lobby=" + Fingerprint(lobbyId) + ": " + string.Format("code={0}, http={1}, message={2}.", error?.Error, error?.HttpCode, error?.ErrorMessage ?? "unavailable")); } } internal static void ManagerError(int code, string systemMessage) { RecordCurrentFailure(PlayFabConnectionErrorMessages.ForParty(code), systemMessage ?? $"PlayFab Party error {code}"); } internal static void PublishBlockingFailure() { object? obj = currentClientSocket?.Target; ZPlayFabSocket val = (ZPlayFabSocket)((obj is ZPlayFabSocket) ? obj : null); if (val != null && Attempts.TryGetValue(val, out var value) && !value.Admitted && value.Failures.Count != 0) { PublishFailures(value); CharacterVaultPlugin.Log?.LogWarning($"Published {value.Failures.Count} blocking PlayFab error(s) for " + $"connection attempt {value.Id}."); } } private static void RecordCurrentFailure(string userMessage, string systemMessage) { object? obj = currentClientSocket?.Target; ZPlayFabSocket val = (ZPlayFabSocket)((obj is ZPlayFabSocket) ? obj : null); if (val != null && Attempts.TryGetValue(val, out var value) && !value.Admitted) { value.AddFailure(userMessage, systemMessage); } } private static void PublishFailures(Attempt attempt) { if (attempt.FailuresPublished) { return; } foreach (Tuple<string, string> failure in attempt.Failures) { CharacterVaultRejection.SetClientMessage(failure.Item1, failure.Item2); } attempt.FailuresPublished = true; } private static string DescribeApiError(string stage, PlayFabError error) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) return $"PlayFab {stage} failed: code={error?.Error}, http={error?.HttpCode}, " + "message=" + (error?.ErrorMessage ?? "unavailable"); } internal static string Fingerprint(string value) { if (string.IsNullOrEmpty(value)) { return "none"; } using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(value)), 0, 4).Replace("-", string.Empty); } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] internal static class PlayFabClientSocketCreatedPatch { private static void Postfix(ZPlayFabSocket __instance) { PlayFabVerboseDiagnostics.Enable(); PlayFabConnectionDiagnostics.Start(__instance); } } [HarmonyPatch(/*Could not decode attribute arguments.*/)] internal static class PlayFabServerSocketCreatedPatch { private static void Postfix() { PlayFabVerboseDiagnostics.Enable(); } } [HarmonyPatch(typeof(ZPlayFabSocket), "ClientConnect")] internal static class PlayFabClientConnectVerboseLoggingPatch { private static void Prefix() { PlayFabVerboseDiagnostics.Enable(); } } [HarmonyPatch(typeof(ZPlayFabSocket), "OnRemotePlayerSessionFound")] internal static class PlayFabSessionFoundPatch { private static void Prefix(ZPlayFabSocket __instance, PlayFabMatchmakingServerData serverData) { PlayFabConnectionDiagnostics.SessionFound(__instance, serverData); } } [HarmonyPatch(typeof(ZPlayFabSocket), "OnRemotePlayerNotFound")] internal static class PlayFabSessionNotFoundPatch { private static void Prefix(ZPlayFabSocket __instance, ZPLayFabMatchmakingFailReason failReason) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) PlayFabConnectionDiagnostics.Failed(__instance, failReason); } } [HarmonyPatch(typeof(ZPlayFabSocket), "OnNetworkJoined")] internal static class PlayFabNetworkJoinedPatch { private static void Prefix(ZPlayFabSocket __instance, string networkId) { PlayFabConnectionDiagnostics.NetworkJoined(__instance, networkId); } } [HarmonyPatch(typeof(ZPlayFabSocket), "Connect")] internal static class PlayFabTransportConnectedPatch { private static void Postfix(ZPlayFabSocket __instance) { PlayFabConnectionDiagnostics.Connected(__instance); } } [HarmonyPatch(typeof(ZPlayFabSocket), "Dispose")] internal static class PlayFabSocketDisposedPatch { private static void Prefix(ZPlayFabSocket __instance) { PlayFabConnectionDiagnostics.Closed(__instance); } } [HarmonyPatch(typeof(ZPlayFabLobbySearch), "OnJoinLobbyFailed")] internal static class PlayFabJoinLobbyFailedPatch { private static void Prefix(PlayFabError error, string lobbyId) { PlayFabConnectionDiagnostics.LobbyFailure("JoinLobby", error, lobbyId); } } [HarmonyPatch(typeof(ZPlayFabLobbySearch), "OnGetLobbyFailed")] internal static class PlayFabGetLobbyFailedPatch { private static void Prefix(PlayFabError error) { PlayFabConnectionDiagnostics.LobbyFailure("GetLobby", error); } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] internal static class PlayFabPeerHandshakeCompletedPatch { private static void Postfix(bool ___m_isServer) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 if (!___m_isServer && (int)ZNet.GetConnectionStatus() == 2) { PlayFabConnectionDiagnostics.Admitted(); } } } [HarmonyPatch(typeof(ServerListGui), "OnSelectedServer")] internal static class PlayFabServerListOriginPatch { private static void Prefix(List<IServerList> ___m_serverLists, int ___m_currentServerList, LocalServerList ___m_favoriteServersList, LocalServerList ___m_recentServersList) { if (___m_currentServerList >= 0 && ___m_currentServerList < ___m_serverLists.Count) { IServerList val = ___m_serverLists[___m_currentServerList]; PlayFabConnectionDiagnostics.SelectOrigin(((object)val == ___m_favoriteServersList) ? "favorite" : (((object)val == ___m_recentServersList) ? "recent" : ((val is FriendsServerList) ? "friends" : ((val is CommunityServerList) ? "community" : "server-list")))); } } } [HarmonyPatch(typeof(FejdStartup), "AutoJoinServer")] internal static class PlayFabJoinCodeOriginPatch { private static void Prefix() { PlayFabConnectionDiagnostics.SelectOrigin("join-code"); } } internal static class PlayFabConnectionErrorMessages { internal static string ForApi(PlayFabError error) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Invalid comparison between Unknown and I4 string text = ((object)Unsafe.As<PlayFabErrorCode, PlayFabErrorCode>(ref error?.Error)/*cast due to .constrained prefix*/).ToString() ?? HttpCode(error); if (IsRateLimited(error)) { return "Too many attempts. Please wait and try again. (Code 429)"; } if (error != null && (int)error.Error == 13003) { return "The server is not accepting connections. (Code " + text + ")"; } return "A PlayFab connection error occurred. Please try again. (Code " + text + ")"; } internal static string ForMatchmaking(ZPLayFabMatchmakingFailReason reason) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected I4, but got Unknown //IL_004b: Unknown result type (might be due to invalid IL or missing references) return (reason - 3) switch { 0 => "The server information is invalid. (Code InvalidServerData)", 1 => "The server is full. (Code ServerFull)", 2 => "PlayFab sign-in failed. Please try again. (Code NotLoggedIn)", 3 => "Too many attempts. Please wait and try again. (Code 429)", 4 => "The server is not reachable. (Code EndPointNotOnInternet)", 5 => "The connection request is invalid. (Code InvalidParameter)", _ => $"A PlayFab connection error occurred. Please try again. (Code {reason})", }; } internal static string ForParty(int code) { return code switch { 11 => "PlayFab is not ready. Please try again. (Code 11)", 4098 => "The PlayFab connection expired. Please try again. (Code 4098)", _ => $"A PlayFab connection error occurred. Please try again. (Code {code})", }; } private static bool IsRateLimited(PlayFabError error) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Invalid comparison between Unknown and I4 //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Invalid comparison between Unknown and I4 if ((error == null || error.HttpCode != 429) && (error == null || (int)error.Error != 1130) && (error == null || (int)error.Error != 1199)) { if (error == null) { return false; } return (int)error.Error == 13008; } return true; } private static string HttpCode(PlayFabError error) { if (error != null && error.HttpCode > 0) { return error.HttpCode.ToString(); } return "Unknown"; } } [HarmonyPatch(typeof(ZNet), "StopAll")] internal static class CharacterVaultZNetStopAllDiagnosticsPatch { private static void Prefix(ZNet __instance, bool suspending) { CharacterVaultPlugin.Log.LogDebug($"PlayFab teardown: entering ZNet.StopAll(suspending={suspending}), " + $"isServer={__instance.IsServer()}, peers={__instance.GetPeers().Count}."); } private static void Postfix(ZNet __instance, bool suspending) { CharacterVaultPlugin.Log.LogDebug($"PlayFab teardown: ZNet.StopAll(suspending={suspending}) returned, " + $"isServer={__instance.IsServer()}, peers={__instance.GetPeers().Count}."); } } [HarmonyPatch(typeof(ZPlayFabSocket), "Dispose")] internal static class CharacterVaultPlayFabSocketDisposeDiagnosticsPatch { private static void Prefix(ZPlayFabSocket __instance) { CharacterVaultPlugin.Log.LogDebug("PlayFab teardown: entering ZPlayFabSocket.Dispose: " + Describe(__instance) + "."); } private static void Postfix(ZPlayFabSocket __instance) { CharacterVaultPlugin.Log.LogDebug("PlayFab teardown: ZPlayFabSocket.Dispose returned: " + Describe(__instance) + "."); } private static string Describe(ZPlayFabSocket socket) { Traverse val = Traverse.Create((object)socket); return string.Format("state={0}, ", val.Field("m_state").GetValue()) + "lobbyId=" + (val.Field<string>("m_lobbyId").Value ?? "<null>") + ", remotePlayerId=" + (socket.m_remotePlayerId ?? "<null>") + ", " + $"connected={socket.IsConnected()}"; } } [HarmonyPatch(typeof(ZPlayFabMatchmaking), "LeaveLobby")] internal static class CharacterVaultLeaveLobbyDiagnosticsPatch { private static void Prefix(string lobbyId) { CharacterVaultPlugin.Log.LogDebug("PlayFab teardown: requesting LeaveLobby for " + (lobbyId ?? "<null>") + "."); } } [HarmonyPatch(typeof(PlayFabMultiplayerManager), "LeaveNetwork")] internal static class CharacterVaultLeaveNetworkDiagnosticsPatch { [CompilerGenerated] private static class <>O { public static OnNetworkLeftHandler <0>__NetworkLeft; public static OnErrorEventHandler <1>__NetworkError; } private static readonly HashSet<PlayFabMultiplayerManager> Observed = new HashSet<PlayFabMultiplayerManager>(); private static void Prefix(PlayFabMultiplayerManager __instance) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) Observe(__instance); CharacterVaultPlugin.Log.LogDebug($"PlayFab teardown: requesting LeaveNetwork: state={__instance.State}, " + "networkId=" + (__instance.NetworkId ?? "<null>") + "."); } private static void Postfix(PlayFabMultiplayerManager __instance) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) CharacterVaultPlugin.Log.LogDebug($"PlayFab teardown: LeaveNetwork returned: state={__instance.State}, " + "networkId=" + (__instance.NetworkId ?? "<null>") + "."); } internal static void Observe(PlayFabMultiplayerManager manager) { //IL_0027: 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_0032: Expected O, but got Unknown //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown if ((Object)(object)manager != (Object)null && Observed.Add(manager)) { object obj = <>O.<0>__NetworkLeft; if (obj == null) { OnNetworkLeftHandler val = NetworkLeft; <>O.<0>__NetworkLeft = val; obj = (object)val; } manager.OnNetworkLeft += (OnNetworkLeftHandler)obj; object obj2 = <>O.<1>__NetworkError; if (obj2 == null) { OnErrorEventHandler val2 = NetworkError; <>O.<1>__NetworkError = val2; obj2 = (object)val2; } manager.OnError += (OnErrorEventHandler)obj2; } } private static void NetworkLeft(object sender, string networkId) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) PlayFabMultiplayerManager val = (PlayFabMultiplayerManager)((sender is PlayFabMultiplayerManager) ? sender : null); CharacterVaultPlugin.Log.LogDebug("PlayFab teardown: OnNetworkLeft completed for networkId=" + (networkId ?? "<null>") + ", state=" + (((val != null) ? ((object)val.State/*cast due to .constrained prefix*/).ToString() : null) ?? "<unknown>") + ", currentNetworkId=" + (((val != null) ? val.NetworkId : null) ?? "<null>") + "."); } private static void NetworkError(object sender, PlayFabMultiplayerManagerErrorArgs args) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) PlayFabMultiplayerManager val = (PlayFabMultiplayerManager)((sender is PlayFabMultiplayerManager) ? sender : null); if (args != null) { PlayFabConnectionDiagnostics.ManagerError(args.Code, args.Message); } CharacterVaultPlugin.Log.LogError($"PlayFab manager error: code={((args != null) ? new int?(args.Code) : ((int?)null))}, type={((args != null) ? new PlayFabMultiplayerManagerErrorType?(args.Type) : ((PlayFabMultiplayerManagerErrorType?)null))}, " + "message=" + (((args != null) ? args.Message : null) ?? "<null>") + ", state=" + (((val != null) ? ((object)val.State/*cast due to .constrained prefix*/).ToString() : null) ?? "<unknown>") + ", networkId=" + (((val != null) ? val.NetworkId : null) ?? "<null>") + "."); } } [HarmonyPatch(typeof(ZPlayFabSocket), "ScheduleResetParty")] internal static class CharacterVaultScheduleResetPartyDiagnosticsPatch { private static void Prefix() { CharacterVaultPlugin.Log.LogWarning("PlayFab recovery: ZPlayFabSocket.ScheduleResetParty requested."); } private static void Postfix() { float value = Traverse.Create(typeof(ZPlayFabSocket)).Field<float>("s_durationToPartyReset").Value; CharacterVaultPlugin.Log.LogWarning($"PlayFab recovery: global ResetParty scheduled in {value:0.000}s."); } } [HarmonyPatch(typeof(ZPlayFabSocket), "ResetPartyTimeout")] internal static class CharacterVaultResetPartyTimeoutDiagnosticsPatch { private static void Prefix(ZPlayFabSocket __instance) { CharacterVaultPlugin.Log.LogWarning("PlayFab recovery: entering socket ResetPartyTimeout. " + Describe(__instance) + "."); } private static void Postfix(ZPlayFabSocket __instance) { Traverse val = Traverse.Create((object)__instance); CharacterVaultPlugin.Log.LogWarning("PlayFab recovery: socket reset timers set: " + string.Format("reconnect={0:0.000}s, ", val.Field<float>("m_partyResetConnectTimeout").Value) + string.Format("reset={0:0.000}s.", val.Field<float>("m_partyResetTimeout").Value)); } private static string Describe(ZPlayFabSocket socket) { Traverse val = Traverse.Create((object)socket); return string.Format("state={0}, ", val.Field("m_state").GetValue()) + "remotePlayerId=" + (socket.m_remotePlayerId ?? "<null>") + ", " + string.Format("isClient={0}", val.Field<bool>("m_isClient").Value); } } [HarmonyPatch(typeof(ZPlayFabSocket), "CancelResetParty")] internal static class CharacterVaultCancelResetPartyDiagnosticsPatch { private static void Prefix(ZPlayFabSocket __instance) { Traverse val = Traverse.Create((object)__instance); CharacterVaultPlugin.Log.LogDebug("PlayFab recovery: CancelResetParty: remotePlayerId=" + (__instance.m_remotePlayerId ?? "<null>") + ", " + string.Format("resetRemaining={0:0.000}s, ", val.Field<float>("m_partyResetTimeout").Value) + string.Format("reconnectRemaining={0:0.000}s.", val.Field<float>("m_partyResetConnectTimeout").Value)); } } [HarmonyPatch(typeof(PlayFabMultiplayerManager), "ResetParty")] internal static class CharacterVaultResetPartyDiagnosticsPatch { private static void Prefix(PlayFabMultiplayerManager __instance) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) CharacterVaultPlugin.Log.LogWarning($"PlayFab recovery: entering ResetParty: state={__instance.State}, " + "networkId=" + (__instance.NetworkId ?? "<null>") + "."); } private static void Postfix(PlayFabMultiplayerManager __instance) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) object value = Traverse.Create((object)__instance).Field("_tasks").GetValue(); CharacterVaultPlugin.Log.LogWarning($"PlayFab recovery: ResetParty queued tasks: state={__instance.State}, " + string.Format("networkId={0}, tasks={1}.", __instance.NetworkId ?? "<null>", value)); } } [HarmonyPatch] internal static class CharacterVaultResetPartyTaskDiagnosticsPatch { private static IEnumerable<MethodBase> TargetMethods() { Type manager = typeof(PlayFabMultiplayerManager); string[] array = new string[4] { "LeaveNetworkTask", "CleanPartyTask", "InitPartyTask", "JoinPartyTask" }; string[] array2 = array; foreach (string text in array2) { Type task = AccessTools.Inner(manager, text); yield return AccessTools.Method(task, "Begin", (Type[])null, (Type[])null); yield return AccessTools.Method(task, "End", (Type[])null, (Type[])null); } } private static void Prefix(object __instance, MethodBase __originalMethod) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) PlayFabMultiplayerManager val = PlayFabMultiplayerManager.Get(); CharacterVaultPlugin.Log.LogDebug("PlayFab recovery task: " + __instance.GetType().Name + "." + __originalMethod.Name + " entering; " + string.Format("managerState={0}, networkId={1}.", val.State, val.NetworkId ?? "<null>")); } private static void Postfix(object __instance, MethodBase __originalMethod) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) PlayFabMultiplayerManager val = PlayFabMultiplayerManager.Get(); CharacterVaultPlugin.Log.LogDebug("PlayFab recovery task: " + __instance.GetType().Name + "." + __originalMethod.Name + " returned; " + string.Format("managerState={0}, networkId={1}.", val.State, val.NetworkId ?? "<null>")); } } internal static class CharacterVaultLobbyLeftDiagnostics { [CompilerGenerated] private static class <>O { public static ZPlayFabMatchmakeLobbyLeftCallback <0>__LobbyLeft; } internal static void Register() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown //IL_0030: 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) //IL_003b: Expected O, but got Unknown object obj = <>O.<0>__LobbyLeft; if (obj == null) { ZPlayFabMatchmakeLobbyLeftCallback val = LobbyLeft; <>O.<0>__LobbyLeft = val; obj = (object)val; } ZPlayFabMatchmaking.LobbyLeft -= (ZPlayFabMatchmakeLobbyLeftCallback)obj; object obj2 = <>O.<0>__LobbyLeft; if (obj2 == null) { ZPlayFabMatchmakeLobbyLeftCallback val2 = LobbyLeft; <>O.<0>__LobbyLeft = val2; obj2 = (object)val2; } ZPlayFabMatchmaking.LobbyLeft += (ZPlayFabMatchmakeLobbyLeftCallback)obj2; } internal static void Unregister() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown object obj = <>O.<0>__LobbyLeft; if (obj == null) { ZPlayFabMatchmakeLobbyLeftCallback val = LobbyLeft; <>O.<0>__LobbyLeft = val; obj = (object)val; } ZPlayFabMatchmaking.LobbyLeft -= (ZPlayFabMatchmakeLobbyLeftCallback)obj; } private static void LobbyLeft(bool success) { CharacterVaultPlugin.Log.LogDebug($"PlayFab teardown: LobbyLeft callback completed with success={success}."); } } internal static class PlayFabEndpointDiagnostics { [CompilerGenerated] private static class <>O { public static OnRemotePlayerJoinedHandler <0>__PlayerJoined; public static OnRemotePlayerLeftHandler <1>__PlayerLeft; public static Func<PlayFabPlayer, string> <2>__DescribePlayer; } [ThreadStatic] private static string pendingSend; [ThreadStatic] private static string resolvedHandles; private static readonly HashSet<PlayFabMultiplayerManager> Observed = new HashSet<PlayFabMultiplayerManager>(); internal static void Observe(PlayFabMultiplayerManager manager) { //IL_0028: 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_0033: Expected O, but got Unknown //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown if (!((Object)(object)manager == (Object)null) && Observed.Add(manager)) { object obj = <>O.<0>__PlayerJoined; if (obj == null) { OnRemotePlayerJoinedHandler val = PlayerJoined; <>O.<0>__PlayerJoined = val; obj = (object)val; } manager.OnRemotePlayerJoined += (OnRemotePlayerJoinedHandler)obj; object obj2 = <>O.<1>__PlayerLeft; if (obj2 == null) { OnRemotePlayerLeftHandler val2 = PlayerLeft; <>O.<1>__PlayerLeft = val2; obj2 = (object)val2; } manager.OnRemotePlayerLeft += (OnRemotePlayerLeftHandler)obj2; CharacterVaultPlugin.Log.LogDebug("PlayFab endpoint diagnostics started: " + DescribeManager(manager) + "."); } } internal static void BeginSend(PlayFabMultiplayerManager manager, IEnumerable<PlayFabPlayer> players, int payloadSize, DeliveryOption delivery) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) pendingSend = "manager=" + DescribeManager(manager) + ", recipients=" + DescribePlayers(players) + ", " + $"payloadBytes={payloadSize}, delivery={delivery}"; resolvedHandles = "not-resolved"; } internal static void HandlesResolved(PARTY_ENDPOINT_HANDLE[] handles) { resolvedHandles = ((handles == null) ? "<null>" : string.Join(",", handles.Select((PARTY_ENDPOINT_HANDLE handle) => ((object)handle).ToString()).ToArray())); } internal static void EndSend(bool succeeded) { if (!succeeded) { CharacterVaultPlugin.Log.LogError("PlayFab SendDataMessage rejected: " + pendingSend + ", endpointHandles=" + resolvedHandles + ". Call stack:\n" + Environment.StackTrace); } pendingSend = null; resolvedHandles = null; } private static void PlayerJoined(object sender, PlayFabPlayer player) { LogPlayerEvent("joined", (PlayFabMultiplayerManager)((sender is PlayFabMultiplayerManager) ? sender : null), player); } private static void PlayerLeft(object sender, PlayFabPlayer player) { LogPlayerEvent("left", (PlayFabMultiplayerManager)((sender is PlayFabMultiplayerManager) ? sender : null), player); } private static void LogPlayerEvent(string action, PlayFabMultiplayerManager manager, PlayFabPlayer player) { CharacterVaultPlugin.Log.LogDebug("PlayFab remote player " + action + ": player=" + DescribePlayer(player) + ", " + DescribeManager(manager) + "."); } private static string DescribeManager(PlayFabMultiplayerManager manager) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)manager == (Object)null) { return "manager=<null>"; } return $"managerObject={RuntimeHelpers.GetHashCode(manager):X8}, state={manager.State}, " + "network=" + PlayFabConnectionDiagnostics.Fingerprint(manager.NetworkId) + ", remotePlayers=" + DescribePlayers(manager.RemotePlayers); } private static string DescribePlayers(IEnumerable<PlayFabPlayer> players) { if (players == null) { return "<null>"; } return "[" + string.Join(",", players.Select(DescribePlayer).ToArray()) + "]"; } private static string DescribePlayer(PlayFabPlayer player) { if (player == null) { return "<null>"; } return PlayFabConnectionDiagnostics.Fingerprint(player.EntityKey?.Id) + "@" + $"{RuntimeHelpers.GetHashCode(player):X8}"; } } [HarmonyPatch(typeof(PlayFabMultiplayerManager), "SendDataMessage", new Type[] { typeof(byte[]), typeof(IEnumerable<PlayFabPlayer>), typeof(DeliveryOption) })] internal static class CharacterVaultPlayFabSendDataDiagnosticsPatch { private static void Prefix(PlayFabMultiplayerManager __instance, byte[] buffer, IEnumerable<PlayFabPlayer> recipients, DeliveryOption deliveryOption) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) PlayFabEndpointDiagnostics.BeginSend(__instance, recipients, (buffer != null) ? buffer.Length : 0, deliveryOption); } private static void Postfix(bool __result) { PlayFabEndpointDiagnostics.EndSend(__result); } } [HarmonyPatch(typeof(PlayFabMultiplayerManager), "EndPointHandlesFromPlayFabPlayerListNoGC")] internal static class CharacterVaultPlayFabEndpointResolutionDiagnosticsPatch { private static void Postfix(PARTY_ENDPOINT_HANDLE[] __result) { PlayFabEndpointDiagnostics.HandlesResolved(__result); } } 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 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_0027: 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) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); string path = playerProfile.GetPath(); BackupSelectedProfile(playerProfile, path); string text = path + ".runic-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; } internal static void BackupSelectedProfile(PlayerProfile selected, string path) { try { byte[] array = Read(selected); string text = Path.Combine(Paths.ConfigPath, "RunicCharacterVault", "client-backups"); Directory.CreateDirectory(text); string text2 = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmssfffffff'Z'", CultureInfo.InvariantCulture); string text3 = VaultStorage.SafeSegment(selected.GetFilename()); string path2 = Path.Combine(text, text3 + "_" + text2 + ".fch"); using (FileStream fileStream = new FileStream(path2, FileMode.CreateNew, FileAccess.Write, FileShare.None, 65536, FileOptions.WriteThrough)) { fileStream.Write(array, 0, array.Length); fileStream.Flush(flushToDisk: true); } if (VaultStorage.Hash(File.ReadAllBytes(path2)) != VaultStorage.Hash(array)) { throw new IOException("Local character backup verification failed."); } BackupRetention.Apply(text, text3); } catch (Exception ex) { CharacterVaultPlugin.Log?.LogWarning("Could not create the local pre-download character backup: " + ex.Message); throw new IOException("Character Vault stopped because it could not preserve your local character backup.", ex); } } 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."); } } } [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-game Disconnect button was pressed."); } } [HarmonyPatch(typeof(Menu), "QuitGame")] internal static class CharacterVaultMenuQuitPatch { private static bool Prefix() { return CharacterVaultPlugin.DisconnectCoordinator?.AllowMenuQuit() ?? true; } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class CharacterVaultStartingItemsPatch { private static void Postfix(Player __instance) { CharacterVaultPlugin.DisconnectCoordinator?.RecordPlayerSpawned(); CharacterVaultPlugin.Transfers?.RecordPlayerSpawned(__instance); } } [HarmonyPatch(typeof(Game), "SpawnPlayer")] internal static class CharacterVaultApplyProfilePatch { private static void Prefix(ref PlayerProfile ___m_playerProfile) { CharacterVaultPlugin.Transfers?.ApplyPendingProfile(ref ___m_playerProfile); } } [HarmonyPatch(typeof(ZNet), "Save")] internal static class CharacterVaultWorldSavePatch { private static void Prefix(ZNet __instance) { WorldSavePolicy.Handle(__instance.IsServer(), delegate { CharacterVaultPlugin.Transfers?.RequestWorldCheckpoint(); }); } } 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 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 = "RunicCharacterVault_Hello_v1"; internal const string AdmissionRpc = "RunicCharacterVault_Admission_v1"; internal const string DownloadBeginRpc = "RunicCharacterVault_DownloadBegin_v1"; internal const string DownloadChunkRpc = "RunicCharacterVault_DownloadChunk_v1"; internal const string DownloadCompleteRpc = "RunicCharacterVault_DownloadComplete_v1"; internal const string UploadBeginRpc = "RunicCharacterVault_UploadBegin_v1"; internal const string UploadChunkRpc = "RunicCharacterVault_UploadChunk_v1"; internal const string UploadCompleteRpc = "RunicCharacterVault_UploadComplete_v1"; internal const string SaveRequestRpc = "RunicCharacterVault_SaveRequest_v1"; internal const string SaveAckRpc = "RunicCharacterVault_SaveAck_v1"; internal const string CommitAckRpc = "RunicCharacterVault_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()); bool flag = NewCharacterPolicy.HasNeverJoinedAWorld(val); if (!flag) { ProfileFile.BackupSelectedProfile(val, val.GetPath()); } val2.Write(flag); serverRpc.Invoke("RunicCharacterVault_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