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 CharactersVault v2.6.1
BepInEx\plugins\CharactersVault\CharactersVault.dll
Decompiled 2 weeks agousing 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; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CharacterVault.Helpers; using CharacterVault.Models; using CharacterVault.Patches; using CharacterVault.Systems; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 CharacterVault { public static class ModConfig { public static ConfigEntry<bool> EnforceCharacterBinding { get; private set; } public static ConfigEntry<float> AutoSaveIntervalMinutes { get; private set; } public static ConfigEntry<string> KickMessageWrongCharacter { get; private set; } public static ConfigEntry<float> ProfileSyncTimeoutSeconds { get; private set; } public static ConfigEntry<bool> ShowCharacterSelectWarning { get; private set; } public static ConfigEntry<bool> VerboseLogging { get; private set; } public static void Initialize(ConfigFile cfg) { EnforceCharacterBinding = cfg.Bind<bool>("Enforcement", "EnforceCharacterBinding", true, "If true, each platform ID may only join with the character name it first registered with."); AutoSaveIntervalMinutes = cfg.Bind<float>("ClientSync", "AutoSaveIntervalMinutes", 5f, "How often (in minutes) the client performs a full profile sync. Default: 5.0"); KickMessageWrongCharacter = cfg.Bind<string>("Messages", "KickMessageWrongCharacter", "Wrong Character", "Message sent to players kicked for using the wrong character."); ProfileSyncTimeoutSeconds = cfg.Bind<float>("ClientSync", "ProfileSyncTimeoutSeconds", 15f, "How long (seconds) the client waits for the server to send its profile data on join. If the server does not respond in time, the client disconnects. Default: 15.0"); ShowCharacterSelectWarning = cfg.Bind<bool>("UI", "ShowCharacterSelectWarning", true, "Show a warning banner on the character selection screen reminding players that existing characters will be wiped on join."); VerboseLogging = cfg.Bind<bool>("Debug", "VerboseLogging", false, "Enable extra debug logging to the BepInEx console/log file."); ClearOrphanedEntries(cfg); } private static void ClearOrphanedEntries(ConfigFile cfg) { try { PropertyInfo property = typeof(ConfigFile).GetProperty("OrphanedEntries", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Dictionary<ConfigDefinition, string> dictionary = null; if (property != null) { dictionary = property.GetValue(cfg) as Dictionary<ConfigDefinition, string>; } if (dictionary == null) { dictionary = (typeof(ConfigFile).GetField("<OrphanedEntries>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic) ?? typeof(ConfigFile).GetField("_orphanedEntries", BindingFlags.Instance | BindingFlags.NonPublic))?.GetValue(cfg) as Dictionary<ConfigDefinition, string>; } if (dictionary != null && dictionary.Count > 0) { Plugin.Log.LogInfo((object)string.Format("[{0}] Removing {1} stale/orphaned config entry/entries from {2}...", "CharactersVault", dictionary.Count, cfg.ConfigFilePath)); dictionary.Clear(); cfg.Save(); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[CharactersVault] Could not clean orphaned config entries: " + ex.Message)); } } } [BepInPlugin("com.charactervault.valheim", "CharactersVault", "2.6.1")] public class Plugin : BaseUnityPlugin { public const string ModGuid = "com.charactervault.valheim"; public const string ModName = "CharactersVault"; public const string ModVersion = "2.6.1"; private Harmony? _harmony; public static Plugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } private void Awake() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; try { Log.LogInfo((object)"══════════════════════════════════════════"); Log.LogInfo((object)" CharactersVault v2.6.1 loading..."); Log.LogInfo((object)"══════════════════════════════════════════"); ModConfig.Initialize(((BaseUnityPlugin)this).Config); DataStore.Initialize(); BindingManager.Load(); _harmony = new Harmony("com.charactervault.valheim"); _harmony.PatchAll(); Log.LogInfo((object)"[CharactersVault] All Harmony patches applied."); Log.LogInfo((object)"[CharactersVault] Loaded successfully. Waiting for network initialization..."); NetworkManager.Initialize(); ClientSyncManager.Initialize(); } catch (Exception arg) { Log.LogError((object)string.Format("[{0}] FATAL: Failed to initialize — {1}", "CharactersVault", arg)); } } private void Start() { } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } Log.LogInfo((object)"[CharactersVault] Unloaded."); } } } namespace CharacterVault.Systems { public static class AdminCommandHandler { public const string RpcAdminCommand = "CharacterVault_AdminCmd"; public const string RpcAdminResponse = "CharacterVault_AdminResp"; public static void RegisterRPCs() { ZRoutedRpc.instance.Register<string>("CharacterVault_AdminCmd", (Action<long, string>)RPC_AdminCommand); ZRoutedRpc.instance.Register<string>("CharacterVault_AdminResp", (Action<long, string>)RPC_AdminResponse); } public static void SendAdminCommand(string text) { if (!string.IsNullOrWhiteSpace(text)) { if (!text.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { text = "/" + text; } if ((Object)(object)ZNet.instance == (Object)null) { DisplayResponse("<color=#FFCC00>[CharactersVault]</color> Not connected to a server."); return; } if (ZNet.instance.IsServer()) { DisplayResponse(ExecuteCommand(text)); return; } Plugin.Log.LogInfo((object)("[AdminCmd] Sending admin command to server: " + text)); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "CharacterVault_AdminCmd", new object[1] { text }); } } private static void RPC_AdminCommand(long sender, string text) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { string playerId = ZNetHelper.GetPlayerId(peer); if (!ZNetHelper.IsAdmin(peer)) { ManualLogSource log = Plugin.Log; string[] obj = new string[6] { "[AdminCmd] Non-admin ", playerId, " (host: ", null, null, null }; ISocket socket = peer.m_socket; obj[3] = ((socket != null) ? socket.GetHostName() : null); obj[4] = ") tried to run: "; obj[5] = text; log.LogWarning((object)string.Concat(obj)); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_AdminResp", new object[1] { "<color=#FF4444>[CharactersVault] Access denied: You are not listed in the server's adminlist.txt.</color>" }); } else { Plugin.Log.LogInfo((object)("[AdminCmd] Executing '" + text + "' for admin " + playerId)); string text2 = ExecuteCommand(text); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_AdminResp", new object[1] { text2 }); } } } private static void RPC_AdminResponse(long sender, string response) { DisplayResponse(response); } private static void DisplayResponse(string response) { Plugin.Log.LogInfo((object)("[AdminCmd] Response:\n" + response)); if ((Object)(object)Chat.instance != (Object)null) { ((Terminal)Chat.instance).AddString(response); } } private static string ExecuteCommand(string text) { string text2 = text; if (text2.StartsWith("/vault", StringComparison.OrdinalIgnoreCase)) { text2 = text2.Substring(6).Trim(); } else if (text2.StartsWith("vault", StringComparison.OrdinalIgnoreCase)) { text2 = text2.Substring(5).Trim(); } else if (text2.StartsWith("/cv", StringComparison.OrdinalIgnoreCase)) { text2 = text2.Substring(3).Trim(); } else if (text2.StartsWith("cv", StringComparison.OrdinalIgnoreCase)) { text2 = text2.Substring(2).Trim(); } string[] array = text2.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0) { return PrintHelp(); } string text3 = array[0].ToLowerInvariant(); switch (text3) { case "wipe": case "remove": case "delete": case "unbind": case "reset": return CmdReset(array); case "list": return CmdList(); case "status": return CmdStatus(array); case "help": return PrintHelp(); default: return "<color=#FFCC00>[CharactersVault]</color> Unknown command '" + text3 + "'. Type <color=#33FF33>/cv help</color> for a list."; } } private static string CmdReset(string[] tokens) { if (tokens.Length < 2) { return "<color=#FFCC00>[CharactersVault]</color> Missing player ID. Example: <color=#33FF33>/cv reset Steam_76561198XXXXXXXXX</color>"; } string text = tokens[1]; bool num = DataStore.WipePlayerData(text); BindingManager.Load(); if (num) { ZNetPeer val = ZNetHelper.FindPeerByPlayerId(text); if (val != null) { NetworkManager.Instance?.RejectPeer(val, "Your character was reset by an administrator. You may reconnect with a fresh character."); } return "<color=#33FF33>[CharactersVault]</color> Reset player " + text + ". Character lock and progression wiped. They may register a fresh character on next join."; } return "<color=#FFCC00>[CharactersVault]</color> No data found for " + text + " — nothing to reset."; } private static string CmdList() { IReadOnlyDictionary<string, CharacterRecord> all = BindingManager.GetAll(); if (all.Count == 0) { return "<color=#FFCC00>[CharactersVault]</color> No character bindings registered."; } StringBuilder stringBuilder = new StringBuilder($"<color=#33CCFF>[CharactersVault]</color> Character Bindings ({all.Count}):\n"); foreach (KeyValuePair<string, CharacterRecord> item in all) { stringBuilder.AppendLine($" <color=#FFCC00>{item.Key}</color> → '{item.Value.CharacterName}' (since {item.Value.RegisteredAt:yyyy-MM-dd})"); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } private static string CmdStatus(string[] tokens) { if (tokens.Length < 2) { return "<color=#FFCC00>[CharactersVault]</color> Missing player ID. Example: <color=#33FF33>/cv status Steam_76561198XXXXXXXXX</color>"; } string text = tokens[1]; string registeredName = BindingManager.GetRegisteredName(text); PlayerSnapshot playerSnapshot = DataStore.LoadSnapshot(text); StringBuilder stringBuilder = new StringBuilder("<color=#33CCFF>[CharactersVault]</color> Status for " + text + ":\n"); stringBuilder.AppendLine(" Binding: " + ((registeredName != null) ? ("'" + registeredName + "'") : "Not registered")); stringBuilder.Append(" Snapshot: " + ((playerSnapshot != null) ? $"Taken {playerSnapshot.SnapshotTime:yyyy-MM-dd HH:mm} UTC" : "None")); return stringBuilder.ToString(); } private static string PrintHelp() { return "<color=#33CCFF>[CharactersVault]</color> Admin Commands:\n <color=#33FF33>/cv list</color> — Show all bindings\n <color=#33FF33>/cv status [playerId]</color> — Show binding + snapshot info\n <color=#33FF33>/cv reset [playerId]</color> — Reset player progression & allow new character registration (aliases: /cv wipe, /cv remove)\n <color=#33FF33>/cv help</color> — This message"; } } public static class BindingManager { private static Dictionary<string, CharacterRecord> _bindings = new Dictionary<string, CharacterRecord>(); public static void Load() { _bindings = DataStore.LoadBindings(); Plugin.Log.LogInfo((object)$"[BindingManager] Loaded {_bindings.Count} character binding(s)."); } private static void Save() { DataStore.SaveBindings(_bindings); } public static bool IsRegistered(string playerId) { return _bindings.ContainsKey(playerId); } public static string? GetRegisteredName(string playerId) { if (!_bindings.TryGetValue(playerId, out CharacterRecord value)) { return null; } return value.CharacterName; } public static void Register(string playerId, string characterName) { _bindings[playerId] = new CharacterRecord { PlayerId = playerId, CharacterName = characterName, RegisteredAt = DateTime.UtcNow, LastSeenAt = DateTime.UtcNow }; Save(); Plugin.Log.LogInfo((object)("[BindingManager] Registered " + playerId + " → '" + characterName + "'")); } public static void RecordJoin(string playerId) { if (_bindings.TryGetValue(playerId, out CharacterRecord value)) { value.LastSeenAt = DateTime.UtcNow; Save(); } } public static bool RemoveBinding(string playerId) { DataStore.DeleteSnapshot(playerId); if (!_bindings.Remove(playerId)) { return false; } Save(); Plugin.Log.LogInfo((object)("[BindingManager] Removed binding and snapshot for " + playerId)); return true; } public static IReadOnlyDictionary<string, CharacterRecord> GetAll() { return new Dictionary<string, CharacterRecord>(_bindings); } } public class ClientSyncManager : MonoBehaviour { private const float DebounceDelaySeconds = 2.5f; private Coroutine? _syncCoroutine; private bool _hasPendingSnapshot; private float _pendingSnapshotTime; private string _pendingReason = string.Empty; public static ClientSyncManager Instance { get; private set; } public static void Initialize() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown GameObject val = new GameObject("CharacterVault_ClientSyncManager"); Instance = val.AddComponent<ClientSyncManager>(); Object.DontDestroyOnLoad((Object)val); } private void Start() { _syncCoroutine = ((MonoBehaviour)this).StartCoroutine(PeriodicSyncCoroutine()); } private void OnDestroy() { if (_syncCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_syncCoroutine); } } private void Update() { if (_hasPendingSnapshot && Time.unscaledTime >= _pendingSnapshotTime) { ExecuteSnapshotUpload(_pendingReason); } } public void QueueSnapshotUpdate(string reason) { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !ClientProfilePatches.IsWaitingForProfile() && !ClientProfilePatches.IsInitializingFirstJoin()) { _hasPendingSnapshot = true; _pendingReason = reason; _pendingSnapshotTime = Time.unscaledTime + 2.5f; } } public void FlushImmediate(string reason) { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !ClientProfilePatches.IsWaitingForProfile() && !ClientProfilePatches.IsInitializingFirstJoin()) { ExecuteSnapshotUpload(reason); } } private void ExecuteSnapshotUpload(string reason) { _hasPendingSnapshot = false; try { byte[] array = ClientProfilePatches.CaptureLivePlayerData(); if (array.Length != 0) { Plugin.Log.LogInfo((object)("[ClientSyncManager] Uploading live player-data checkpoint: " + reason + ".")); NetworkManager.Instance.SendProfileDataToServer(array, isPlayerData: true); } } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientSyncManager] Failed to upload live player-data checkpoint: {arg}"); } } private IEnumerator PeriodicSyncCoroutine() { while (true) { float num = ModConfig.AutoSaveIntervalMinutes.Value * 60f; yield return (object)new WaitForSeconds(num); try { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer() && !((Object)(object)Game.instance == (Object)null)) { Plugin.Log.LogInfo((object)"[ClientSyncManager] Running periodic profile sync to server."); ClientProfilePatches.SafeSavePlayerProfile(setLogoutPoint: true); } } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientSyncManager] Exception during periodic sync: {arg}"); } } } } public static class ConnectionRejectionManager { private static string? _reason; public static void SetReason(string reason) { _reason = reason; } public static string? ConsumeReason() { string? reason = _reason; _reason = null; return reason; } } public static class DataStore { private static string _rootDir = string.Empty; private static string _snapshotsDir = string.Empty; public static string BindingsFilePath => Path.Combine(_rootDir, "bindings.json"); private static string SnapshotPath(string playerId) { return Path.Combine(_snapshotsDir, playerId + ".json"); } public static void Initialize() { _rootDir = Path.Combine(Paths.ConfigPath, "CharacterVault"); _snapshotsDir = Path.Combine(_rootDir, "snapshots"); Directory.CreateDirectory(_rootDir); Directory.CreateDirectory(_snapshotsDir); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Root Data Directory: " + _rootDir)); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Snapshots Directory: " + _snapshotsDir)); } public static Dictionary<string, CharacterRecord> LoadBindings() { try { if (!File.Exists(BindingsFilePath)) { return new Dictionary<string, CharacterRecord>(); } return SimpleJson.DeserializeObject<Dictionary<string, CharacterRecord>>(File.ReadAllText(BindingsFilePath)) ?? new Dictionary<string, CharacterRecord>(); } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load bindings from '" + BindingsFilePath + "': " + ex.Message)); return new Dictionary<string, CharacterRecord>(); } } public static void SaveBindings(Dictionary<string, CharacterRecord> bindings) { try { string content = SimpleJson.SerializeObject(bindings); WriteAllTextAtomically(BindingsFilePath, content); Plugin.Log.LogInfo((object)$"[CharacterVault :: DataStore] Saved {bindings.Count} binding(s) to '{BindingsFilePath}'"); } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to save bindings: " + ex.Message)); } } public static PlayerSnapshot? LoadSnapshot(string playerId) { string text = SnapshotPath(playerId); try { if (!File.Exists(text)) { Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] No snapshot file found at '" + text + "'")); return null; } PlayerSnapshot result = SimpleJson.DeserializeObject<PlayerSnapshot>(File.ReadAllText(text)); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Loaded snapshot file for platform ID " + playerId + " from '" + text + "'")); return result; } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load snapshot for platform ID " + playerId + ": " + ex.Message)); return null; } } public static void SaveSnapshot(PlayerSnapshot snapshot) { try { string text = SnapshotPath(snapshot.PlayerId); string content = SimpleJson.SerializeObject(snapshot); WriteAllTextAtomically(text, content); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Saved snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "') -> '" + text + "'")); } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to save snapshot for platform ID " + snapshot.PlayerId + ": " + ex.Message)); } } public static bool DeleteSnapshot(string playerId) { string path = SnapshotPath(playerId); if (File.Exists(path)) { try { File.Delete(path); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Deleted snapshot file for platform ID " + playerId + ".")); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to delete snapshot for " + playerId + ": " + ex.Message)); } } return false; } public static bool WipePlayerData(string playerId) { bool result = DeleteSnapshot(playerId); Dictionary<string, CharacterRecord> dictionary = LoadBindings(); if (dictionary.ContainsKey(playerId)) { dictionary.Remove(playerId); SaveBindings(dictionary); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Wipe: removed character binding for platform ID " + playerId + ".")); result = true; } return result; } private static void WriteAllTextAtomically(string path, string content) { string text = $"{path}.{Guid.NewGuid():N}.tmp"; try { File.WriteAllText(text, content); if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } finally { if (File.Exists(text)) { File.Delete(text); } } } } public class NetworkManager : MonoBehaviour { private sealed class IncomingTransfer { public int TotalChunks { get; } public Dictionary<int, byte[]> Chunks { get; } = new Dictionary<int, byte[]>(); public int TotalBytes { get; set; } public float LastUpdated { get; set; } public IncomingTransfer(int totalChunks) { TotalChunks = totalChunks; LastUpdated = Time.unscaledTime; } } private const string RpcHandshake = "CharacterVault_Handshake"; private const string RpcProfileData = "CharacterVault_ProfileData"; private const string RpcSaveProfile = "CharacterVault_SaveProfile"; private const string RpcSaveProfileChunk = "CharacterVault_SaveProfileChunk"; private const string RpcProfileDataChunk = "CharacterVault_ProfileDataChunk"; private const string RpcKickReason = "CharacterVault_KickReason"; private const int ChunkSize = 512000; private const int MaxProfileBytes = 67108864; private const float IncomingTransferTimeoutSeconds = 120f; private readonly HashSet<long> _handshakeCompleted = new HashSet<long>(); private readonly Dictionary<long, IncomingTransfer> _incomingChunks = new Dictionary<long, IncomingTransfer>(); private float _nextIncomingTransferCleanup; public static NetworkManager Instance { get; private set; } public static void Initialize() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown GameObject val = new GameObject("CharacterVault_NetworkManager"); Instance = val.AddComponent<NetworkManager>(); Object.DontDestroyOnLoad((Object)val); Plugin.Log.LogInfo((object)"[CharacterVault :: Network] NetworkManager initialized."); } public void RegisterRPCs() { ZRoutedRpc.instance.Register<string>("CharacterVault_Handshake", (Action<long, string>)RPC_Handshake); ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("CharacterVault_ProfileDataChunk", (Method<int, int, bool, bool, ZPackage>)RPC_ProfileDataChunk); ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("CharacterVault_SaveProfileChunk", (Method<int, int, bool, bool, ZPackage>)RPC_SaveProfileChunk); ZRoutedRpc.instance.Register<string>("CharacterVault_KickReason", (Action<long, string>)RPC_KickReason); AdminCommandHandler.RegisterRPCs(); Plugin.Log.LogInfo((object)"[CharacterVault :: Network] RPC handlers registered."); } private void RPC_KickReason(long sender, string reason) { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { ConnectionRejectionManager.SetReason(reason); Plugin.Log.LogWarning((object)("[CharacterVault :: Network] Server rejection reason: " + reason)); } } public void RejectPeer(ZNetPeer peer, string reason) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Rejecting peer {peer.m_uid} (platform ID {ZNetHelper.GetPlayerId(peer)}): {reason}"); ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "CharacterVault_KickReason", new object[1] { reason }); ((MonoBehaviour)this).StartCoroutine(DisconnectRejectedPeer(peer)); } } private IEnumerator DisconnectRejectedPeer(ZNetPeer peer) { yield return (object)new WaitForSecondsRealtime(1f); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.GetPeer(peer.m_uid) == peer) { peer.m_rpc.Invoke("Error", new object[1] { 12 }); ZNet.instance.Disconnect(peer); } } private void RPC_Handshake(long sender, string version) { if (ZNet.instance.IsServer()) { if (version == "request") { return; } if (version != "2.6.1") { Plugin.Log.LogWarning((object)string.Format("[CharacterVault :: Network] Peer {0} wrong mod version: '{1}' (expected '{2}'). KICKING.", sender, version, "2.6.1")); ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { RejectPeer(peer, "CharactersVault version mismatch: client has v" + version + ", server requires v2.6.1"); } return; } Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} COMPLETED HANDSHAKE (v{version})."); _handshakeCompleted.Add(sender); ZNetPeer peer2 = ZNet.instance.GetPeer(sender); if (peer2 == null) { Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Peer {sender} null after handshake!"); return; } string playerId = ZNetHelper.GetPlayerId(peer2); Plugin.Log.LogInfo((object)("[CharacterVault :: Network] Checking snapshot store for platform ID " + playerId + " ('" + peer2.m_playerName + "')...")); PlayerSnapshot snapshot = SnapshotManager.GetSnapshot(playerId); bool flag = snapshot != null && snapshot.HasData && string.Equals(snapshot.CharacterName, peer2.m_playerName, StringComparison.OrdinalIgnoreCase); byte[] array; if (flag) { array = snapshot.GetProfileBytes(); Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> sending existing stored profile for '{snapshot.CharacterName}' ({array.Length} bytes)."); } else { if (snapshot != null && !string.IsNullOrEmpty(snapshot.CharacterName) && !string.Equals(snapshot.CharacterName, peer2.m_playerName, StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> stored snapshot is for character '{snapshot.CharacterName}', but player joined with '{peer2.m_playerName}'. Discarding old snapshot."); SnapshotManager.DeleteSnapshot(playerId); } Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> no valid snapshot for '{peer2.m_playerName}' (first join). Requesting client-side clean initialization."); array = Array.Empty<byte>(); } SendProfileDataToClient(sender, array, flag && snapshot.IsPlayerData, !flag); } else if (version == "request") { Plugin.Log.LogInfo((object)"[CharacterVault :: Network] Server requested handshake. Replying with version '2.6.1'."); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_Handshake", new object[1] { "2.6.1" }); } } public void SendHandshakeRequest(ZNetPeer peer) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Initiating handshake with peer {peer.m_uid} (platform ID {peer.m_socket.GetHostName()})..."); ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "CharacterVault_Handshake", new object[1] { "request" }); ((MonoBehaviour)this).StartCoroutine(HandshakeTimeout(peer)); } private IEnumerator HandshakeTimeout(ZNetPeer peer) { float timeout = ModConfig.ProfileSyncTimeoutSeconds.Value; yield return (object)new WaitForSecondsRealtime(timeout); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.GetPeer(peer.m_uid) != null && !_handshakeCompleted.Contains(peer.m_uid)) { Plugin.Log.LogWarning((object)($"[CharacterVault :: Network] Peer {peer.m_uid} TIMED OUT after {timeout}s waiting for handshake. " + "Client mod not installed or incompatible. KICKING PEER.")); RejectPeer(peer, "CharactersVault handshake timeout: server did not receive client handshake reply."); } } public void OnPeerDisconnected(long peerId) { _incomingChunks.Remove(peerId); if (_handshakeCompleted.Remove(peerId)) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Cleaned up handshake tracking for disconnected peer {peerId}."); } } private void SendProfileDataToClient(long peerId, byte[] data, bool isPlayerData, bool isFirstJoin) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Transmitting {data.Length} profile bytes to client peer {peerId}..."); SendChunks(peerId, "CharacterVault_ProfileDataChunk", data, isPlayerData, isFirstJoin); } public void SendProfileDataToServer(byte[] data, bool isPlayerData = false) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Transmitting {data.Length} profile bytes to server..."); SendChunks(0L, "CharacterVault_SaveProfileChunk", data, isPlayerData, isFirstJoin: false); } private void SendChunks(long target, string rpcName, byte[] data, bool isPlayerData, bool isFirstJoin) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown int num = Mathf.CeilToInt((float)data.Length / 512000f); if (num == 0) { ZRoutedRpc.instance.InvokeRoutedRPC(target, rpcName, new object[5] { 0, 0, isPlayerData, isFirstJoin, (object)new ZPackage() }); return; } for (int i = 0; i < num; i++) { int num2 = Mathf.Min(512000, data.Length - i * 512000); byte[] array = new byte[num2]; Array.Copy(data, i * 512000, array, 0, num2); ZRoutedRpc.instance.InvokeRoutedRPC(target, rpcName, new object[5] { num, i, isPlayerData, isFirstJoin, (object)new ZPackage(array) }); } } private void RPC_ProfileDataChunk(long sender, int totalChunks, int chunkIndex, bool isPlayerData, bool isFirstJoin, ZPackage chunk) { if (!ZNet.instance.IsServer()) { byte[] array = ProcessIncomingChunk(sender, totalChunks, chunkIndex, chunk.GetArray()); if (array != null) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] CLIENT: Full profile data reassembled ({array.Length} bytes). Passing to ClientProfilePatches."); ClientProfilePatches.ReceiveServerProfile(array, isPlayerData, isFirstJoin); } } } private void RPC_SaveProfileChunk(long sender, int totalChunks, int chunkIndex, bool isPlayerData, bool isFirstJoin, ZPackage chunk) { if (!ZNet.instance.IsServer()) { return; } byte[] array = ProcessIncomingChunk(sender, totalChunks, chunkIndex, chunk.GetArray()); if (array == null) { return; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer == null) { return; } string playerId = ZNetHelper.GetPlayerId(peer); if (ModConfig.EnforceCharacterBinding.Value) { string registeredName = BindingManager.GetRegisteredName(playerId); if (string.IsNullOrEmpty(registeredName) || !string.Equals(registeredName, peer.m_playerName, StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogWarning((object)string.Format("[CharacterVault :: Network] Ignored profile upload from peer {0} (platform ID {1}, character '{2}') because they are not currently bound to this character (registered: '{3}').", sender, playerId, peer.m_playerName, registeredName ?? "none")); return; } } Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Server received full profile upload ({array.Length} bytes) from peer {sender} (platform ID {playerId}, Character '{peer.m_playerName}')"); SnapshotManager.SaveSnapshot(SnapshotManager.CreateSnapshot(playerId, peer.m_playerName, array, isPlayerData)); } private byte[]? ProcessIncomingChunk(long sender, int totalChunks, int chunkIndex, byte[] chunk) { if (totalChunks == 0) { return Array.Empty<byte>(); } int num = Mathf.CeilToInt(131.072f); if (totalChunks < 1 || totalChunks > num || chunkIndex < 0 || chunkIndex >= totalChunks || chunk.Length > 512000) { Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Discarded malformed chunk transfer from peer {sender}."); _incomingChunks.Remove(sender); return null; } if (!_incomingChunks.TryGetValue(sender, out IncomingTransfer value) || value.TotalChunks != totalChunks) { value = new IncomingTransfer(totalChunks); _incomingChunks[sender] = value; } value.LastUpdated = Time.unscaledTime; if (value.Chunks.TryGetValue(chunkIndex, out byte[] value2)) { value.TotalBytes -= value2.Length; } value.TotalBytes += chunk.Length; if (value.TotalBytes > 67108864) { Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Discarded oversized profile transfer from peer {sender}."); _incomingChunks.Remove(sender); return null; } value.Chunks[chunkIndex] = chunk; if (value.Chunks.Count == totalChunks) { using (MemoryStream memoryStream = new MemoryStream(value.TotalBytes)) { for (int i = 0; i < totalChunks; i++) { if (!value.Chunks.TryGetValue(i, out byte[] value3)) { _incomingChunks.Remove(sender); return null; } memoryStream.Write(value3, 0, value3.Length); } _incomingChunks.Remove(sender); return memoryStream.ToArray(); } } return null; } private void Update() { if (Time.unscaledTime < _nextIncomingTransferCleanup) { return; } _nextIncomingTransferCleanup = Time.unscaledTime + 10f; List<long> list = new List<long>(); foreach (KeyValuePair<long, IncomingTransfer> incomingChunk in _incomingChunks) { if (Time.unscaledTime - incomingChunk.Value.LastUpdated > 120f) { list.Add(incomingChunk.Key); } } foreach (long item in list) { _incomingChunks.Remove(item); Plugin.Log.LogWarning((object)$"[CharacterVault :: Network] Discarded incomplete profile transfer from peer {item} after timeout."); } } } public static class SnapshotManager { public static PlayerSnapshot CreateSnapshot(string playerId, string characterName, byte[] profileBytes, bool isPlayerData) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Snapshot] Creating snapshot for platform ID {playerId} ('{characterName}'), raw bytes length: {((profileBytes != null) ? profileBytes.Length : 0)}"); return new PlayerSnapshot { PlayerId = playerId, CharacterName = characterName, SnapshotTime = DateTime.UtcNow, IsPlayerData = isPlayerData, ProfileDataBase64 = ((profileBytes != null && profileBytes.Length != 0) ? Convert.ToBase64String(profileBytes) : string.Empty) }; } public static PlayerSnapshot? GetSnapshot(string playerId) { PlayerSnapshot playerSnapshot = DataStore.LoadSnapshot(playerId); if (playerSnapshot != null && playerSnapshot.HasData) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Snapshot] Found existing snapshot for platform ID {playerId} ('{playerSnapshot.CharacterName}'), created: {playerSnapshot.SnapshotTime}"); } else { Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] No existing snapshot found for platform ID " + playerId + ".")); } return playerSnapshot; } public static void SaveSnapshot(PlayerSnapshot snapshot) { Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] Saving snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "')...")); DataStore.SaveSnapshot(snapshot); } public static bool DeleteSnapshot(string playerId) { Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] Deleting snapshot for platform ID " + playerId + "...")); return DataStore.DeleteSnapshot(playerId); } } } namespace CharacterVault.Patches { [HarmonyPatch(typeof(Terminal), "TryRunCommand")] public static class Terminal_TryRunCommand_Patch { [HarmonyPrefix] public static bool Prefix(Terminal __instance, string text) { try { if (string.IsNullOrWhiteSpace(text)) { return true; } string text2 = text.Trim(); if (text2.StartsWith("cv ", StringComparison.OrdinalIgnoreCase) || text2.Equals("cv", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("/cv ", StringComparison.OrdinalIgnoreCase) || text2.Equals("/cv", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("vault ", StringComparison.OrdinalIgnoreCase) || text2.Equals("vault", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("/vault ", StringComparison.OrdinalIgnoreCase) || text2.Equals("/vault", StringComparison.OrdinalIgnoreCase)) { AdminCommandHandler.SendAdminCommand(text2); return false; } } catch (Exception arg) { Plugin.Log.LogError((object)$"[TerminalPatch] Error in TryRunCommand prefix: {arg}"); } return true; } } public static class ClientProfilePatches { private static byte[]? _serverProfileData; private static bool _waitingForProfile; internal static bool FirstJoinInitializationPending; internal static bool IsFirstJoinInitializationActive; public static void ReceiveServerProfile(byte[] profileData, bool isPlayerData, bool isFirstJoin) { Plugin.Log.LogInfo((object)$"[ClientProfilePatches] Received {profileData.Length} bytes from server."); if (isFirstJoin) { FirstJoinInitializationPending = true; _waitingForProfile = false; Plugin.Log.LogInfo((object)"[ClientProfilePatches] First join confirmed. Preserving appearance and clearing gameplay state after local profile load."); if ((Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile() != null && (Object)(object)Player.m_localPlayer != (Object)null) { try { Traverse.Create((object)Game.instance.GetPlayerProfile()).Method("LoadPlayerData", new object[1] { Player.m_localPlayer }).GetValue(); Plugin.Log.LogInfo((object)"[ClientProfilePatches] Triggered initial LoadPlayerData for live Player on first join."); return; } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to trigger LoadPlayerData on first join: {arg}"); return; } } return; } if (profileData == null || profileData.Length == 0) { Plugin.Log.LogError((object)"[ClientProfilePatches] Server returned an empty profile. Keeping local profile blocked."); return; } _serverProfileData = profileData; if ((Object)(object)Game.instance != (Object)null && Game.instance.GetPlayerProfile() != null) { PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); string name = playerProfile.GetName(); if (isPlayerData ? ApplyServerPlayerData(playerProfile, profileData) : WriteServerDataToDisk(playerProfile, profileData)) { if (!isPlayerData) { try { Traverse.Create((object)playerProfile).Method("LoadPlayerFromDisk", Array.Empty<object>()).GetValue(); playerProfile.SetName(name); Plugin.Log.LogInfo((object)"[ClientProfilePatches] Reloaded PlayerProfile memory from server data."); } catch (Exception ex) { Plugin.Log.LogError((object)("[ClientProfilePatches] Failed to reload PlayerProfile from disk: " + ex.Message)); } } _waitingForProfile = false; if ((Object)(object)Player.m_localPlayer != (Object)null) { try { Traverse.Create((object)playerProfile).Method("LoadPlayerData", new object[1] { Player.m_localPlayer }).GetValue(); Plugin.Log.LogInfo((object)"[ClientProfilePatches] Re-applied server profile data to live Player instance!"); } catch (Exception ex2) { Plugin.Log.LogError((object)("[ClientProfilePatches] Failed to apply server profile to live Player: " + ex2.Message)); } } } } _waitingForProfile = false; } public static void ExpectServerProfile() { _serverProfileData = null; _waitingForProfile = true; Plugin.Log.LogInfo((object)"[ClientProfilePatches] Waiting for server profile data..."); } public static bool IsWaitingForProfile() { return _waitingForProfile; } public static bool IsInitializingFirstJoin() { return IsFirstJoinInitializationActive; } public static byte[]? GetServerProfile() { return _serverProfileData; } public static void Reset() { _serverProfileData = null; _waitingForProfile = false; FirstJoinInitializationPending = false; IsFirstJoinInitializationActive = false; } public static void SafeSavePlayerProfile(bool setLogoutPoint) { if ((Object)(object)Game.instance == (Object)null) { return; } try { Game.instance.SavePlayerProfile(setLogoutPoint, false); } catch (MissingMethodException) { try { MethodInfo method = typeof(Game).GetMethod("SavePlayerProfile", new Type[1] { typeof(bool) }); if (method != null) { method.Invoke(Game.instance, new object[1] { setLogoutPoint }); return; } } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientProfilePatches] Legacy SavePlayerProfile invocation failed: {arg}"); } throw; } } private static bool WriteServerDataToDisk(PlayerProfile profile, byte[] data) { //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_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Invalid comparison between Unknown and I4 //IL_0052: Unknown result type (might be due to invalid IL or missing references) if (data == null || data.Length == 0) { Plugin.Log.LogWarning((object)"[WriteServerDataToDisk] Server sent empty profile bytes — skipping disk overwrite."); return false; } try { string path = profile.GetPath(); FileSource fileSource = GetFileSource(profile); FileWriter val = new FileWriter(path, (CloudStorageFileGrouping)1, (FileHelperType)0, fileSource); val.m_binary.Write(data); val.Finish(); if ((int)val.Status != 2) { throw new IOException($"Valheim failed to write the profile ({val.Status})."); } Plugin.Log.LogInfo((object)$"[WriteServerDataToDisk] Wrote {data.Length} server bytes to '{path}'."); return true; } catch (Exception arg) { Plugin.Log.LogError((object)$"[WriteServerDataToDisk] Failed to write server profile to disk: {arg}"); return false; } } public static byte[] ReadProfileBytes(PlayerProfile profile) { //IL_0007: 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_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown string path = profile.GetPath(); FileSource fileSource = GetFileSource(profile); FileReader val = new FileReader(path, fileSource, (FileHelperType)0); try { int count = (int)(val.m_binary.BaseStream.Length - val.m_binary.BaseStream.Position); return val.m_binary.ReadBytes(count); } finally { val.Dispose(); } } public static byte[] CaptureLivePlayerData() { if ((Object)(object)Game.instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null) { return Array.Empty<byte>(); } PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); if (playerProfile == null) { return Array.Empty<byte>(); } playerProfile.SavePlayerData(Player.m_localPlayer); return (byte[])Traverse.Create((object)playerProfile).Field("m_playerData").GetValue(); } private static bool ApplyServerPlayerData(PlayerProfile profile, byte[] data) { try { Traverse.Create((object)profile).Field("m_playerData").SetValue((object)data); Plugin.Log.LogInfo((object)$"[ClientProfilePatches] Applied {data.Length} bytes of authoritative live player data."); return true; } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to apply live player data: {arg}"); return false; } } private static FileSource GetFileSource(PlayerProfile profile) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) return (FileSource)Traverse.Create((object)profile).Field("m_fileSource").GetValue(); } } [HarmonyPatch(typeof(Game), "Start")] public static class Game_Start_Patch { [HarmonyPrefix] public static void Prefix(Game __instance) { if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer()) { ClientProfilePatches.ExpectServerProfile(); NetworkManager.Instance.RegisterRPCs(); } else if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { NetworkManager.Instance.RegisterRPCs(); } } } [HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")] public static class PlayerProfile_SavePlayerToDisk_Patch { [HarmonyPostfix] public static void Postfix(PlayerProfile __instance) { if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } if (ClientProfilePatches.IsWaitingForProfile()) { Plugin.Log.LogWarning((object)"[ClientProfilePatches] Suppressed profile upload to server (still waiting for authoritative server profile)."); return; } Plugin.Log.LogInfo((object)"[ClientProfilePatches] Player saved — uploading profile to server."); try { byte[] data = ClientProfilePatches.ReadProfileBytes(__instance); NetworkManager.Instance.SendProfileDataToServer(data); } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to upload saved profile to server: {arg}"); } } } [HarmonyPatch(typeof(PlayerProfile), "LoadPlayerData")] public static class PlayerProfile_LoadPlayerData_Patch { [HarmonyPrefix] public static bool Prefix() { if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer() && ClientProfilePatches.IsWaitingForProfile()) { Plugin.Log.LogInfo((object)"[ClientProfilePatches] Blocked local profile data while waiting for authoritative server profile."); return false; } return true; } [HarmonyPostfix] public static void Postfix(PlayerProfile __instance, Player player) { if (!ClientProfilePatches.FirstJoinInitializationPending) { return; } ClientProfilePatches.FirstJoinInitializationPending = false; ClientProfilePatches.IsFirstJoinInitializationActive = true; try { ((Humanoid)player).UnequipAllItems(); ((Humanoid)player).GetInventory().RemoveAll(); ((Humanoid)player).GiveDefaultItems(); player.SetGuardianPower(string.Empty); Traverse.Create((object)player).Field("m_skills").GetValue<Skills>() .Clear(); player.m_customData.Clear(); ClearPlayerCollection(player, "m_foods"); ClearPlayerCollection(player, "m_knownRecipes"); ClearPlayerCollection(player, "m_knownStations"); ClearPlayerCollection(player, "m_knownMaterial"); ClearPlayerCollection(player, "m_shownTutorials"); ClearPlayerCollection(player, "m_uniques"); ClearPlayerCollection(player, "m_trophies"); ClearPlayerCollection(player, "m_knownBiome"); ClearPlayerCollection(player, "m_knownTexts"); __instance.SavePlayerData(player); byte[] data = (byte[])Traverse.Create((object)__instance).Field("m_playerData").GetValue(); NetworkManager.Instance.SendProfileDataToServer(data, isPlayerData: true); Plugin.Log.LogInfo((object)"[ClientProfilePatches] Created initial clean player snapshot while preserving local appearance."); } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to initialize first-join player state: {arg}"); } finally { ClientProfilePatches.IsFirstJoinInitializationActive = false; } } private static void ClearPlayerCollection(Player player, string fieldName) { Traverse.Create((object)player).Field(fieldName).Method("Clear", Array.Empty<object>()) .GetValue(); } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class ZNet_Disconnect_SaveProfile_Patch { [HarmonyPrefix] public static void Prefix() { if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || ClientProfilePatches.IsWaitingForProfile() || (Object)(object)Game.instance == (Object)null || Game.instance.GetPlayerProfile() == null) { return; } try { Plugin.Log.LogInfo((object)"[ClientProfilePatches] Saving profile before server disconnect."); ClientSyncManager.Instance?.FlushImmediate("disconnect flush"); ClientProfilePatches.SafeSavePlayerProfile(setLogoutPoint: true); } catch (Exception arg) { Plugin.Log.LogError((object)$"[ClientProfilePatches] Failed to save profile before disconnect: {arg}"); } } } [HarmonyPatch] public static class Inventory_Change_Checkpoint_Patch { public static IEnumerable<MethodBase> TargetMethods() { foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(Inventory))) { if (declaredMethod.Name == "Changed") { yield return declaredMethod; } } } [HarmonyPostfix] public static void Postfix(Inventory __instance) { if (LocalPlayerState.IsInventory(__instance)) { ClientSyncManager.Instance.QueueSnapshotUpdate("inventory change"); } } } [HarmonyPatch] public static class Skills_Change_Checkpoint_Patch { public static IEnumerable<MethodBase> TargetMethods() { foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(Skills))) { if (declaredMethod.Name == "LowerAllSkills" || declaredMethod.Name == "OnDeath") { yield return declaredMethod; } } } [HarmonyPostfix] public static void Postfix(Skills __instance) { if (LocalPlayerState.IsSkills(__instance)) { ClientSyncManager.Instance.QueueSnapshotUpdate("skill change"); } } } [HarmonyPatch(typeof(Player), "OnSkillLevelup")] public static class Player_SkillLevelup_Checkpoint_Patch { [HarmonyPostfix] public static void Postfix(Player __instance) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer)) { ClientSyncManager.Instance.QueueSnapshotUpdate("skill level up"); } } } [HarmonyPatch] public static class Player_GuardianPower_Checkpoint_Patch { public static IEnumerable<MethodBase> TargetMethods() { foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(typeof(Player))) { if (declaredMethod.Name == "SetGuardianPower" || declaredMethod.Name == "SetForsakenPower") { yield return declaredMethod; } } } [HarmonyPrefix] public static void Prefix(Player __instance, out string __state) { __state = __instance.GetGuardianPowerName(); } [HarmonyPostfix] public static void Postfix(Player __instance, string __state) { if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !(__state == __instance.GetGuardianPowerName())) { ClientSyncManager.Instance.QueueSnapshotUpdate("forsaken power change"); } } } internal static class LocalPlayerState { public static bool IsInventory(Inventory inventory) { if ((Object)(object)Player.m_localPlayer == (Object)null) { return false; } return Traverse.Create((object)Player.m_localPlayer).Field("m_inventory").GetValue() == inventory; } public static bool IsSkills(Skills skills) { if ((Object)(object)Player.m_localPlayer == (Object)null) { return false; } return Traverse.Create((object)Player.m_localPlayer).Field("m_skills").GetValue() == skills; } } [HarmonyPatch(typeof(FejdStartup))] public static class MenuWarningPatch { private static GameObject? _warningBanner; private const string WarningText = "<color=#FFCC00><b>CharactersVault:</b> Joining a server with this mod will wipe your selected character's items & skills!\nCreate a new character, and use that character on the server.</color>"; [HarmonyPatch("SetupGui")] [HarmonyPostfix] public static void SetupGui_Postfix(FejdStartup __instance) { EnsureWarningBanner(__instance); } [HarmonyPatch("ShowCharacterSelection")] [HarmonyPostfix] public static void ShowCharacterSelection_Postfix(FejdStartup __instance) { EnsureWarningBanner(__instance); } private static void EnsureWarningBanner(FejdStartup startup) { //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) try { if (!ModConfig.ShowCharacterSelectWarning.Value) { if ((Object)(object)_warningBanner != (Object)null) { _warningBanner.SetActive(false); } } else if ((Object)(object)_warningBanner != (Object)null) { _warningBanner.SetActive(true); } else { if ((Object)(object)startup == (Object)null || (Object)(object)startup.m_selectCharacterPanel == (Object)null) { return; } Component val = Traverse.Create((object)startup).Field("m_csSourceInfo").GetValue<Component>() ?? Traverse.Create((object)startup).Field("m_csName").GetValue<Component>() ?? Traverse.Create((object)startup).Field("m_versionLabel").GetValue<Component>(); if ((Object)(object)val == (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[MenuWarningPatch] Could not find reference text component to create character select warning banner."); } return; } Transform val2 = (((Object)(object)startup.m_characterSelectScreen != (Object)null) ? startup.m_characterSelectScreen.transform : startup.m_selectCharacterPanel.transform); _warningBanner = Object.Instantiate<GameObject>(val.gameObject, val2, false); ((Object)_warningBanner).name = "CharactersVault_SelectWarningBanner"; RectTransform component = _warningBanner.GetComponent<RectTransform>(); if ((Object)(object)component != (Object)null) { component.anchorMin = new Vector2(0.5f, 1f); component.anchorMax = new Vector2(0.5f, 1f); component.pivot = new Vector2(0.5f, 1f); component.anchoredPosition = new Vector2(0f, -40f); component.sizeDelta = new Vector2(950f, 60f); } Component val3 = _warningBanner.GetComponent("TMP_Text") ?? _warningBanner.GetComponent("TextMeshProUGUI"); if ((Object)(object)val3 != (Object)null) { Traverse obj = Traverse.Create((object)val3); obj.Property("text", (object[])null).SetValue((object)"<color=#FFCC00><b>CharactersVault:</b> Joining a server with this mod will wipe your selected character's items & skills!\nCreate a new character, and use that character on the server.</color>"); obj.Property("fontSize", (object[])null).SetValue((object)15f); obj.Property("alignment", (object[])null).SetValue((object)2); obj.Property("enableWordWrapping", (object[])null).SetValue((object)true); obj.Property("richText", (object[])null).SetValue((object)true); } _warningBanner.SetActive(true); ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogInfo((object)"[MenuWarningPatch] Character select warning banner created successfully."); } } } catch (Exception ex) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("[MenuWarningPatch] Failed to create character select warning banner: " + ex.Message)); } } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] public static class ZNet_RPC_PeerInfo_Patch { [HarmonyPostfix] public static void Postfix(ZNet __instance, ZRpc rpc) { try { if (!__instance.IsServer()) { return; } ZNetPeer peerByRpc = ZNetHelper.GetPeerByRpc(rpc); if (peerByRpc == null) { return; } string playerId = ZNetHelper.GetPlayerId(peerByRpc); string playerName = peerByRpc.m_playerName; if (!ZNetHelper.IsValidPlayerId(playerId) || string.IsNullOrWhiteSpace(playerName)) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogInfo((object)"[ZNetPatch] Rejecting peer with invalid platform ID or empty name."); } KickPeer(peerByRpc, "Invalid player identity."); return; } Plugin.Log.LogInfo((object)("[CharacterVault] Player joining: platformId=" + playerId + ", Character='" + playerName + "'")); if (ModConfig.EnforceCharacterBinding.Value) { if (BindingManager.IsRegistered(playerId)) { string registeredName = BindingManager.GetRegisteredName(playerId); if (!string.Equals(registeredName, playerName, StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogWarning((object)("[CharacterVault] KICK " + playerId + ": tried '" + playerName + "', registered as '" + registeredName + "'")); KickPeer(peerByRpc, ModConfig.KickMessageWrongCharacter.Value); return; } } else { BindingManager.Register(playerId, playerName); } } BindingManager.RecordJoin(playerId); NetworkManager.Instance.SendHandshakeRequest(peerByRpc); } catch (Exception arg) { Plugin.Log.LogError((object)$"[ZNetPatch] Exception in RPC_PeerInfo patch: {arg}"); } } private static void KickPeer(ZNetPeer peer, string reason) { try { Plugin.Log.LogWarning((object)$"[CharacterVault] Kicking peer {peer.m_uid}: {reason}"); NetworkManager.Instance.RejectPeer(peer, reason); } catch (Exception ex) { Plugin.Log.LogError((object)("[ZNetPatch] Error sending kick RPC: " + ex.Message)); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class ZNet_Disconnect_Patch { [HarmonyPrefix] public static void Prefix(ZNetPeer peer) { try { if (peer != null) { NetworkManager.Instance?.OnPeerDisconnected(peer.m_uid); } } catch (Exception arg) { Plugin.Log.LogError((object)$"[ZNetPatch] Exception in Disconnect patch: {arg}"); } } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] public static class FejdStartup_ShowConnectError_Patch { [HarmonyPostfix] public static void Postfix(FejdStartup __instance) { try { string text = ConnectionRejectionManager.ConsumeReason(); if (!string.IsNullOrWhiteSpace(text)) { Traverse.Create((object)__instance).Field("m_connectionFailedError").Property("text", (object[])null) .SetValue((object)text); Plugin.Log.LogInfo((object)("[ShowConnectError] Displaying rejection reason: " + text)); } } catch (Exception ex) { Plugin.Log.LogError((object)("[ShowConnectError] Failed to set error text: " + ex.Message)); } } } } namespace CharacterVault.Models { public class CharacterRecord { public string PlayerId { get; set; } = string.Empty; public string CharacterName { get; set; } = string.Empty; public DateTime RegisteredAt { get; set; } = DateTime.UtcNow; public DateTime LastSeenAt { get; set; } = DateTime.UtcNow; } public class PlayerSnapshot { public string PlayerId { get; set; } = string.Empty; public string CharacterName { get; set; } = string.Empty; public DateTime SnapshotTime { get; set; } = DateTime.UtcNow; public string ProfileDataBase64 { get; set; } = string.Empty; public bool IsPlayerData { get; set; } public bool HasData => !string.IsNullOrEmpty(ProfileDataBase64); public byte[] GetProfileBytes() { if (!string.IsNullOrEmpty(ProfileDataBase64)) { return Convert.FromBase64String(ProfileDataBase64); } return Array.Empty<byte>(); } } } namespace CharacterVault.Helpers { public static class ProfileHelper { private static bool _initialized; private static MethodInfo? _savePlayerToDiskMethod; private static FieldInfo? _playerNameField; private static FieldInfo? _filenameField; private static FieldInfo? _fileSourceField; private static void EnsureInitialized() { if (_initialized) { return; } _initialized = true; try { Type? typeFromHandle = typeof(PlayerProfile); _savePlayerToDiskMethod = typeFromHandle.GetMethod("SavePlayerToDisk", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _playerNameField = typeFromHandle.GetField("m_playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _filenameField = typeFromHandle.GetField("m_filename", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _fileSourceField = typeFromHandle.GetField("m_fileSource", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (_savePlayerToDiskMethod == null) { Plugin.Log.LogWarning((object)"[CharacterVault :: ProfileHelper] Could not find PlayerProfile.SavePlayerToDisk method via reflection."); } if (_playerNameField == null || _filenameField == null) { Plugin.Log.LogWarning((object)"[CharacterVault :: ProfileHelper] Could not find name/filename fields on PlayerProfile."); } } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] Reflection init failed: " + ex.Message)); } } public static byte[] CreateBlankProfile(string characterName) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Expected O, but got Unknown Plugin.Log.LogInfo((object)("[CharacterVault :: ProfileHelper] Generating native blank .fch profile for character '" + characterName + "'...")); EnsureInitialized(); if (_savePlayerToDiskMethod == null) { Plugin.Log.LogError((object)"[CharacterVault :: ProfileHelper] Reflection targets missing — cannot generate blank profile."); return Array.Empty<byte>(); } try { Type typeFromHandle = typeof(PlayerProfile); PlayerProfile val = null; if (_fileSourceField != null) { try { object obj = Enum.Parse(_fileSourceField.FieldType, "Local"); val = (PlayerProfile)Activator.CreateInstance(typeFromHandle, characterName, obj); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[CharacterVault :: ProfileHelper] Activator.CreateInstance failed: " + ex.Message)); } } if (val == null) { Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] Failed to instantiate PlayerProfile for '" + characterName + "'.")); return Array.Empty<byte>(); } val.SetName(characterName); _savePlayerToDiskMethod.Invoke(val, null); string path = val.GetPath(); if (File.Exists(path)) { byte[] array = File.ReadAllBytes(path); try { File.Delete(path); } catch { } Plugin.Log.LogInfo((object)$"[CharacterVault :: ProfileHelper] SUCCESS: Generated {array.Length}-byte native blank profile for '{characterName}'."); return array; } Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] SavePlayerToDisk did not produce expected file at '" + path + "'.")); return Array.Empty<byte>(); } catch (Exception ex2) { if (ex2 is TargetInvocationException { InnerException: not null } ex3) { Plugin.Log.LogError((object)("[CharacterVault :: ProfileHelper] Failed to generate blank profile for '" + characterName + "': " + ex3.InnerException.GetType().Name + " - " + ex3.InnerException.Message + "\nStackTrace:\n" + ex3.InnerException.StackTrace)); } else { Plugin.Log.LogError((object)$"[CharacterVault :: ProfileHelper] Failed to generate blank profile for '{characterName}': {ex2}"); } return Array.Empty<byte>(); } } } public static class SimpleJson { public static string SerializeObject(object? obj, bool prettyPrint = true) { StringBuilder stringBuilder = new StringBuilder(); SerializeValue(obj, stringBuilder, (!prettyPrint) ? (-1) : 0); return stringBuilder.ToString(); } public static T? DeserializeObject<T>(string json) where T : new() { if (string.IsNullOrWhiteSpace(json)) { return default(T); } object obj = Deserialize(json); if (obj == null) { return default(T); } if (obj is T) { return (T)obj; } if (typeof(IDictionary).IsAssignableFrom(typeof(T)) && obj is IDictionary<string, object> dictionary) { Type[] genericArguments = typeof(T).GetGenericArguments(); if (genericArguments.Length == 2 && genericArguments[0] == typeof(string)) { Type targetType = genericArguments[1]; IDictionary dictionary2 = (IDictionary)Activator.CreateInstance(typeof(T)); foreach (KeyValuePair<string, object> item in dictionary) { if (item.Value is IDictionary<string, object> dict) { object value = ConvertDictionaryToObject(dict, targetType); dictionary2.Add(item.Key, value); } else { dictionary2.Add(item.Key, ConvertValue(item.Value, targetType)); } } return (T)dictionary2; } } if (obj is IDictionary<string, object> dict2) { return (T)ConvertDictionaryToObject(dict2, typeof(T)); } return default(T); } private static object ConvertDictionaryToObject(IDictionary<string, object?> dict, Type targetType) { object obj = Activator.CreateInstance(targetType); PropertyInfo[] properties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); FieldInfo[] fields = targetType.GetFields(BindingFlags.Instance | BindingFlags.Public); PropertyInfo[] array = properties; foreach (PropertyInfo propertyInfo in array) { if (propertyInfo.CanWrite && FindKey(dict, propertyInfo.Name, out object value)) { propertyInfo.SetValue(obj, ConvertValue(value, propertyInfo.PropertyType), null); } } FieldInfo[] array2 = fields; foreach (FieldInfo fieldInfo in array2) { if (FindKey(dict, fieldInfo.Name, out object value2)) { fieldInfo.SetValue(obj, ConvertValue(value2, fieldInfo.FieldType)); } } return obj; } private static bool FindKey(IDictionary<string, object?> dict, string name, out object? value) { if (dict.TryGetValue(name, out value)) { return true; } foreach (KeyValuePair<string, object> item in dict) { if (string.Equals(item.Key, name, StringComparison.OrdinalIgnoreCase)) { value = item.Value; return true; } } value = null; return false; } private static object? ConvertValue(object? val, Type targetType) { if (val == null) { return null; } if (targetType.IsAssignableFrom(val.GetType())) { return val; } if (targetType == typeof(DateTime)) { if (val is string s && DateTime.TryParse(s, null, DateTimeStyles.RoundtripKind, out var result)) { return result; } return default(DateTime); } if (targetType == typeof(bool)) { if (val is bool flag) { return flag; } if (val is string value && bool.TryParse(value, out var result2)) { return result2; } return false; } if (targetType == typeof(int)) { return Convert.ToInt32(val, CultureInfo.InvariantCulture); } if (targetType == typeof(long)) { return Convert.ToInt64(val, CultureInfo.InvariantCulture); } if (targetType == typeof(float)) { return Convert.ToSingle(val, CultureInfo.InvariantCulture); } if (targetType == typeof(double)) { return Convert.ToDouble(val, CultureInfo.InvariantCulture); } if (targetType == typeof(string)) { return val.ToString(); } return val; } private static void SerializeValue(object? value, StringBuilder sb, int indentLevel) { if (value == null) { sb.Append("null"); } else if (value is string str) { sb.Append('"').Append(EscapeString(str)).Append('"'); } else if (value is bool flag) { sb.Append(flag ? "true" : "false"); } else if (value is DateTime dateTime) { sb.Append('"').Append(dateTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)).Append('"'); } else if (value is int || value is long || value is short || value is byte) { sb.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); } else if (value is float num) { sb.Append(num.ToString("R", CultureInfo.InvariantCulture)); } else if (value is double num2) { sb.Append(num2.ToString("R", CultureInfo.InvariantCulture)); } else if (value is IDictionary dict) { SerializeDictionary(dict, sb, indentLevel); } else if (value is IEnumerable list) { SerializeList(list, sb, indentLevel); } else { SerializeObjectFields(value, sb, indentLevel); } } private static void SerializeDictionary(IDictionary dict, StringBuilder sb, int indentLevel) { bool flag = indentLevel >= 0; if (dict.Count == 0) { sb.Append("{}"); return; } sb.Append('{'); if (flag) { sb.Append('\n'); } int num = 0; foreach (DictionaryEntry item in dict) { if (flag) { sb.Append(' ', (indentLevel + 1) * 2); } sb.Append('"').Append(EscapeString(item.Key?.ToString() ?? string.Empty)).Append("\":"); if (flag) { sb.Append(' '); } SerializeValue(item.Value, sb, flag ? (indentLevel + 1) : (-1)); if (++num < dict.Count) { sb.Append(','); } if (flag) { sb.Append('\n'); } } if (flag) { sb.Append(' ', indentLevel * 2); } sb.Append('}'); } private static void SerializeList(IEnumerable list, StringBuilder sb, int indentLevel) { bool flag = indentLevel >= 0; sb.Append('['); bool flag2 = true; foreach (object item in list) { if (!flag2) { sb.Append(flag ? ", " : ","); } SerializeValue(item, sb, flag ? (indentLevel + 1) : (-1)); flag2 = false; } sb.Append(']'); } private static void SerializeObjectFields(object obj, StringBuilder sb, int indentLevel) { bool flag = indentLevel >= 0; Type type = obj.GetType(); PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); List<(string, object)> list = new List<(string, object)>(); PropertyInfo[] array = properties; foreach (PropertyInfo propertyInfo in array) { if (propertyInfo.CanRead && propertyInfo.CanWrite) { list.Add((propertyInfo.Name, propertyInfo.GetValue(obj, null))); } } FieldInfo[] array2 = fields; foreach (FieldInfo fieldInfo in array2) { list.Add((fieldInfo.Name, fieldInfo.GetValue(obj))); } if (list.Count == 0) { sb.Append("{}"); return; } sb.Append('{'); if (flag) { sb.Append('\n'); } for (int j = 0; j < list.Count; j++) { if (flag) { sb.Append(' ', (indentLevel + 1) * 2); } sb.Append('"').Append(EscapeString(list[j].Item1)).Append("\":"); if (flag) { sb.Append(' '); } SerializeValue(list[j].Item2, sb, flag ? (indentLevel + 1) : (-1)); if (j < list.Count - 1) { sb.Append(','); } if (flag) { sb.Append('\n'); } } if (flag) { sb.Append(' ', indentLevel * 2); } sb.Append('}'); } private static string EscapeString(string str) { StringBuilder stringBuilder = new StringBuilder(str.Length + 4); foreach (char c in str) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\b': stringBuilder.Append("\\b"); continue; case '\f': stringBuilder.Append("\\f"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\r': stringBuilder.Append("\\r"); continue; case '\t': stringBuilder.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("x4")); } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } public static object? Deserialize(string json) { int index = 0; return ParseValue(json, ref index); } private static object? ParseValue(string json, ref int index) { SkipWhitespace(json, ref index); if (index >= json.Length) { return null; } char c = json[index]; switch (c) { case '{': return ParseObject(json, ref index); case '[': return ParseArray(json, ref index); case '"': return ParseString(json, ref index); case 'f': case 't': return ParseBool(json, ref index); case 'n': return ParseNull(json, ref index); default: if (char.IsDigit(c) || c == '-') { return ParseNumber(json, ref index); } return null; } } private static Dictionary<string, object?> ParseObject(string json, ref int index) { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); index++; while (index < json.Length) { SkipWhitespace(json, ref index); if (index >= json.Length) { break; } if (json[index] == '}') { index++; break; } string key = ParseString(json, ref index); SkipWhitespace(json, ref index); if (index < json.Length && json[index] == ':') { index++; } object value = ParseValue(json, ref index); dictionary[key] = value; SkipWhitespace(json, ref index); if (index < json.Length && json[index] == ',') { index++; } } return dictionary; } private static List<object?> ParseArray(string json, ref int index) { List<object> list = new List<object>(); index++; while (index < json.Length) { SkipWhitespace(json, ref index); if (index >= json.Length) { break; } if (json[index] == ']') { index++; break; } list.Add(ParseValue(json, ref index)); SkipWhitespace(json, ref index); if (index < json.Length && json[index] == ',') { index++; } } return list; } private static string ParseString(string json, ref int index) { StringBuilder stringBuilder = new StringBuilder(); index++; while (index < json.Length) { char c = json[index++]; if (c == '"') { break; } if (c == '\\' && index < json.Length) { char c2 = json[index++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (index + 4 <= json.Length && int.TryParse(json.Substring(index, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { stringBuilder.Append((char)result); index += 4; } break; } default: stringBuilder.Append(c2); break; } } else { stringBuilder.Append(c); } } return stringBuilder.ToString(); } private static bool ParseBool(string json, ref int index) { if (json.Substring(index).StartsWith("true", StringComparison.OrdinalIgnoreCase)) { index += 4; return true; } if (json.Substring(index).StartsWith("false", StringComparison.OrdinalIgnoreCase)) { index += 5; return false; } index++; return false; } private static object? ParseNull(string json, ref int index) { if (json.Substring(index).StartsWith("null", StringComparison.OrdinalIgnoreCase)) { index += 4; } return null; } private static object ParseNumber(string json, ref int index) { int num = index; while (index < json.Length && (char.IsDigit(json[index]) || json[index] == '-' || json[index] == '+' || json[index] == '.' || json[index] == 'e' || json[index] == 'E')) { index++; } string text = json.Substring(num, index - num); long result2; if (text.Contains(".") || text.Contains("e") || text.Contains("E")) { if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } } else if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out result2)) { if (result2 <= int.MaxValue && result2 >= int.MinValue) { return (int)result2; } return result2; } return text; } private static void SkipWhitespace(string json, ref int index) { while (index < json.Length && char.IsWhiteSpace(json[index])) { index++; } } } internal static class ZNetHelper { private static readonly FieldInfo? FiPeers = typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? FiAdminList = typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo? MiListContainsId = typeof(ZNet).GetMethod("ListContainsId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); public static List<ZNetPeer> GetPeers() { if ((Object)(object)ZNet.instance == (Object)null || FiPeers == null) { return new List<ZNetPeer>(); } if (!(FiPeers.GetValue(ZNet.instance) is List<ZNetPeer> collection)) { return new List<ZNetPeer>(); } return new List<ZNetPeer>(collection); } public static ZNetPeer? GetPeerByRpc(ZRpc rpc) { return ((IEnumerable<ZNetPeer>)GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_rpc == rpc)); } public static ZNetPeer? FindPeerByPlayerId(string playerId) { if (string.IsNullOrWhiteSpace(playerId)) { return null; } return ((IEnumerable<ZNetPeer>)GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p != null && string.Equals(GetPlayerId(p), playerId, StringComparison.OrdinalIgnoreCase))); } public static string GetPlayerId(ZNetPeer peer) { if (peer == null || peer.m_socket == null) { return string.Empty; } string hostName = peer.m_socket.GetHostName(); if (string.IsNullOrWhiteSpace(hostName)) { return string.Empty; } if (ulong.TryParse(hostName, out var _)) { return "Steam_" + hostName; } return hostName; } public static bool IsValidPlayerId(string? playerId) { if (string.IsNullOrWhiteSpace(playerId) || playerId.Length > 128) { return false; } foreach (char c in playerId) { if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') { return false; } } ulong result; if (!playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase) && !playerId.StartsWith("Xbox_", StringComparison.OrdinalIgnoreCase) && !playerId.StartsWith("PlayFab_", StringComparison.OrdinalIgnoreCase)) { return ulong.TryParse(playerId, out result); } return true; } public static bool IsAdmin(ZNetPeer peer) { if (peer == null) { return false; } string playerId = GetPlayerId(peer); ISocket socket = peer.m_socket; string text = ((socket != null) ? socket.GetHostName() : null) ?? string.Empty; if (!IsAdmin(playerId)) { if (!string.IsNullOrWhiteSpace(text)) { return IsAdmin(text); } return false; } return true; } public static bool IsAdmin(string playerId) { if ((Object)(object)ZNet.instance == (Object)null || string.IsNullOrWhiteSpace(playerId)) { return false; } try { object obj = ((FiAdminList != null) ? FiAdminList.GetValue(ZNet.instance) : Traverse.Create((object)ZNet.instance).Field("m_adminList").GetValue()); if (obj == null) { return false; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { playerId.Trim() }; ulong result; if (playerId.StartsWith("Steam_", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(playerId.Substring("Steam_".Length).Trim()); } else if (ulong.TryParse(playerId.Trim(), out result)) { hashSet.Add("Steam_" + playerId.Trim()); } if (playerId.StartsWith("Xbox_", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(playerId.Substring("Xbox_".Length).Trim()); } if (playerId.StartsWith("PlayFab_", StringComparison.OrdinalIgnoreCase)) { hashSet.Add(playerId.Substring("PlayFab_".Length).Trim()); } if (MiListContainsId != null) { foreach (string item in hashSet) { if ((bool)(MiListContainsId.Invoke(ZNet.instance, new object[2] { obj, item }) ?? ((object)false))) { return true; } } } MethodInfo method = obj.GetType().GetMethod("Contains", new Type[1] { typeof(string) }); if (method != null) { foreach (string item2 in hashSet) { if ((bool)(method.Invoke(obj, new object[1] { item2 }) ?? ((object)false))) { return true; } } } MethodInfo method2 = obj.GetType().GetMethod("GetList"); if (method2 != null && method2.Invoke(obj, null) is IEnumerable enumerable) { foreach (object item3 in enumerable) { if (item3 is string text && hashSet.Contains(text.Trim())) { return true; } } } return false; } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)("[ZNetHelper] IsAdmin check failed: " + ex.Message)); } return false; } } } }