using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.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 Newtonsoft.Json; 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 EnforceCharacterBinding { get; private set; } public static ConfigEntry EnforceInventorySnapshot { get; private set; } public static ConfigEntry EnforceSkillSnapshot { get; private set; } public static ConfigEntry ZdoSyncWaitSeconds { get; private set; } public static ConfigEntry ZdoSyncMaxWaitSeconds { get; private set; } public static ConfigEntry AutoSaveIntervalMinutes { get; private set; } public static ConfigEntry KickMessageWrongCharacter { get; private set; } public static ConfigEntry KickMessageMismatch { get; private set; } public static ConfigEntry ProfileSyncTimeoutSeconds { get; private set; } public static ConfigEntry VerboseLogging { get; private set; } public static void Initialize(ConfigFile cfg) { EnforceCharacterBinding = cfg.Bind("Enforcement", "EnforceCharacterBinding", true, "If true, each Steam ID may only join with the character name it first registered with."); EnforceInventorySnapshot = cfg.Bind("Enforcement", "EnforceInventorySnapshot", true, "If true, kick players whose inventory differs from the server's last known snapshot."); EnforceSkillSnapshot = cfg.Bind("Enforcement", "EnforceSkillSnapshot", true, "If true, kick players whose skills differ from the server's last known snapshot."); ZdoSyncWaitSeconds = cfg.Bind("Snapshot", "ZdoSyncWaitSeconds", 1f, "Seconds to wait between ZDO polling attempts after a player connects. Default: 1.0"); ZdoSyncMaxWaitSeconds = cfg.Bind("Snapshot", "ZdoSyncMaxWaitSeconds", 90f, "Maximum seconds to wait for the player's ZDO to populate before aborting the check. Default: 90.0"); AutoSaveIntervalMinutes = cfg.Bind("Snapshot", "AutoSaveIntervalMinutes", 5f, "How often (in minutes) to auto-save snapshots for all connected players. Prevents data loss on unclean shutdowns. Default: 5.0"); KickMessageWrongCharacter = cfg.Bind("Messages", "KickMessageWrongCharacter", "Wrong Character", "Message sent to players kicked for using the wrong character."); KickMessageMismatch = cfg.Bind("Messages", "KickMessageMismatch", "CharacterVault: Your character data does not match the server's records. Contact an admin if you believe this is a mistake.", "Message sent to players kicked for inventory/skill mismatch."); ProfileSyncTimeoutSeconds = cfg.Bind("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"); VerboseLogging = cfg.Bind("Debug", "VerboseLogging", false, "Enable extra debug logging to the BepInEx console/log file."); } } [BepInPlugin("com.charactervault.valheim", "CharactersVault", "2.3.0")] public class Plugin : BaseUnityPlugin { public const string ModGuid = "com.charactervault.valheim"; public const string ModName = "CharactersVault"; public const string ModVersion = "2.3.0"; 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.3.0 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 { private const string Prefix = "/sc"; public static bool IsCommand(string text) { return text.StartsWith("/sc", StringComparison.OrdinalIgnoreCase); } public static bool Handle(long senderUid, string text) { ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(senderUid) : null); if (val == null) { return false; } string playerId = ZNetHelper.GetPlayerId(val); if (!IsAdmin(playerId)) { Plugin.Log.LogWarning((object)("[AdminCmd] Non-admin " + playerId + " tried to run: " + text)); return false; } string[] array = text.Substring("/sc".Length).Trim().Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0) { LogToAdmin(val, PrintHelp()); return true; } string text2 = array[0].ToLowerInvariant(); switch (text2) { case "allow": return CmdAllow(val, array); case "deny": return CmdDeny(val, array); case "remove": return CmdRemove(val, array); case "wipe": return CmdWipe(val, array); case "list": return CmdList(val); case "status": return CmdStatus(val, array); case "help": LogToAdmin(val, PrintHelp()); return true; default: LogToAdmin(val, "Unknown command '" + text2 + "'. Type /sc help for a list."); return true; } } private static bool CmdAllow(ZNetPeer adminPeer, string[] tokens) { if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId)) { return true; } OverrideManager.GrantMemoryOverride(targetId); HashSet hashSet = DataStore.LoadOverrides(); hashSet.Add(targetId); DataStore.SaveOverrides(hashSet); LogToAdmin(adminPeer, "Granted one-time override for " + targetId + ". They may join once with mismatched data."); return true; } private static bool CmdDeny(ZNetPeer adminPeer, string[] tokens) { if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId)) { return true; } bool flag = OverrideManager.RevokeOverride(targetId); HashSet hashSet = DataStore.LoadOverrides(); bool flag2 = hashSet.Remove(targetId); if (flag2) { DataStore.SaveOverrides(hashSet); } LogToAdmin(adminPeer, (flag || flag2) ? ("Revoked override for " + targetId + ".") : ("No active override found for " + targetId + ".")); return true; } private static bool CmdRemove(ZNetPeer adminPeer, string[] tokens) { if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId)) { return true; } bool flag = BindingManager.RemoveBinding(targetId); LogToAdmin(adminPeer, flag ? ("Removed binding for " + targetId + ". They may re-register with a new character.") : ("No binding found for " + targetId + ".")); return true; } private static bool CmdWipe(ZNetPeer adminPeer, string[] tokens) { if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId)) { return true; } if (DataStore.WipePlayerData(targetId)) { LogToAdmin(adminPeer, "Wiped all server data for " + targetId + ". On their next join they will receive a blank character (no items, no skills)."); } else { LogToAdmin(adminPeer, "No data found for " + targetId + " — nothing to wipe."); } return true; } private static bool CmdList(ZNetPeer adminPeer) { IReadOnlyDictionary all = BindingManager.GetAll(); if (all.Count == 0) { LogToAdmin(adminPeer, "No character bindings registered."); return true; } StringBuilder stringBuilder = new StringBuilder($"Character Bindings ({all.Count}):\n"); foreach (KeyValuePair item in all) { stringBuilder.AppendLine($" {item.Key} → '{item.Value.CharacterName}' (since {item.Value.RegisteredAt:yyyy-MM-dd})"); } LogToAdmin(adminPeer, stringBuilder.ToString().TrimEnd(Array.Empty())); return true; } private static bool CmdStatus(ZNetPeer adminPeer, string[] tokens) { if (!TryGetPlayerId(tokens, 1, adminPeer, out string targetId)) { return true; } string registeredName = BindingManager.GetRegisteredName(targetId); PlayerSnapshot playerSnapshot = DataStore.LoadSnapshot(targetId); bool flag = OverrideManager.HasOverride(targetId); StringBuilder stringBuilder = new StringBuilder("Status for " + targetId + ":\n"); stringBuilder.AppendLine(" Binding: " + ((registeredName != null) ? ("'" + registeredName + "'") : "Not registered")); stringBuilder.AppendLine(" Snapshot: " + ((playerSnapshot != null) ? $"Taken {playerSnapshot.SnapshotTime:yyyy-MM-dd HH:mm} UTC" : "None")); stringBuilder.Append(" Override: " + (flag ? "ACTIVE (will consume on next join)" : "None")); LogToAdmin(adminPeer, stringBuilder.ToString()); return true; } private static bool TryGetPlayerId(string[] tokens, int index, ZNetPeer adminPeer, out string targetId) { targetId = ""; if (tokens.Length <= index) { LogToAdmin(adminPeer, "Invalid or missing player ID. Example: /sc " + tokens[0] + " Steam_76561198XXXXXXXXX"); return false; } targetId = tokens[index]; return true; } private static bool IsAdmin(string playerId) { if ((Object)(object)ZNet.instance != (Object)null) { return ZNetHelper.IsAdmin(playerId); } return false; } private static void LogToAdmin(ZNetPeer adminPeer, string message) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) Plugin.Log.LogInfo((object)("[AdminCmd] → " + message)); try { if (adminPeer != null) { ZRoutedRpc.instance.InvokeRoutedRPC(adminPeer.m_uid, "ChatMessage", new object[4] { adminPeer.m_refPos, 1, "CharacterVault", message }); } } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("[AdminCmd] Could not send in-game response: " + ex.Message)); } } } private static string PrintHelp() { return "CharacterVault Admin Commands:\n /sc allow [playerId] — One-time override for mismatched player\n /sc deny [playerId] — Revoke pending override\n /sc remove [playerId] — Remove character binding (allows re-register)\n /sc wipe [playerId] — Delete ALL server data for player (blank slate next join)\n /sc list — Show all bindings\n /sc status [playerId] — Show binding + snapshot info\n /sc help — This message\n Override file: " + DataStore.OverridesFilePath; } } public static class BindingManager { private static Dictionary _bindings = new Dictionary(); 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) { if (!_bindings.Remove(playerId)) { return false; } Save(); Plugin.Log.LogInfo((object)("[BindingManager] Removed binding for " + playerId)); return true; } public static IReadOnlyDictionary GetAll() { return _bindings; } } public class ClientSyncManager : MonoBehaviour { private Coroutine? _syncCoroutine; 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(); Object.DontDestroyOnLoad((Object)val); } private void Start() { _syncCoroutine = ((MonoBehaviour)this).StartCoroutine(PeriodicSyncCoroutine()); } private void OnDestroy() { if (_syncCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_syncCoroutine); } } public void QueueSnapshotUpdate(string reason) { if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || ClientProfilePatches.IsWaitingForProfile() || ClientProfilePatches.IsInitializingFirstJoin()) { return; } 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."); Game.instance.SavePlayerProfile(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; private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { Formatting = (Formatting)1, NullValueHandling = (NullValueHandling)1 }; public static string BindingsFilePath => Path.Combine(_rootDir, "bindings.json"); public static string OverridesFilePath => Path.Combine(_rootDir, "overrides.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 LoadBindings() { try { if (!File.Exists(BindingsFilePath)) { return new Dictionary(); } return JsonConvert.DeserializeObject>(File.ReadAllText(BindingsFilePath), JsonSettings) ?? new Dictionary(); } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load bindings from '" + BindingsFilePath + "': " + ex.Message)); return new Dictionary(); } } public static void SaveBindings(Dictionary bindings) { try { string contents = JsonConvert.SerializeObject((object)bindings, JsonSettings); File.WriteAllText(BindingsFilePath, contents); 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 = JsonConvert.DeserializeObject(File.ReadAllText(text), JsonSettings); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] Loaded snapshot file for SteamID " + playerId + " from '" + text + "'")); return result; } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load snapshot for SteamID " + playerId + ": " + ex.Message)); return null; } } public static void SaveSnapshot(PlayerSnapshot snapshot) { try { string text = SnapshotPath(snapshot.PlayerId); string contents = JsonConvert.SerializeObject((object)snapshot, JsonSettings); File.WriteAllText(text, contents); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] SUCCESSFULLY SAVED SNAPSHOT for SteamID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "') -> '" + text + "'")); } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to save snapshot for SteamID " + snapshot.PlayerId + ": " + ex.Message)); } } public static HashSet LoadOverrides() { try { if (!File.Exists(OverridesFilePath)) { return new HashSet(); } Dictionary obj = JsonConvert.DeserializeObject>(File.ReadAllText(OverridesFilePath), JsonSettings) ?? new Dictionary(); HashSet hashSet = new HashSet(); foreach (KeyValuePair item in obj) { if (item.Value) { hashSet.Add(item.Key); } } return hashSet; } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to load overrides: " + ex.Message)); return new HashSet(); } } public static bool WipePlayerData(string playerId) { bool result = false; string path = SnapshotPath(playerId); if (File.Exists(path)) { try { File.Delete(path); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] WIPE: Deleted snapshot file for SteamID " + playerId + ".")); result = true; } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] WIPE FAILED: Could not delete snapshot for " + playerId + ": " + ex.Message)); } } Dictionary dictionary = LoadBindings(); if (dictionary.ContainsKey(playerId)) { dictionary.Remove(playerId); SaveBindings(dictionary); Plugin.Log.LogInfo((object)("[CharacterVault :: DataStore] WIPE: Removed character binding for SteamID " + playerId + ".")); result = true; } return result; } public static void SaveOverrides(HashSet overrides) { try { Dictionary dictionary = new Dictionary(); foreach (string @override in overrides) { dictionary[@override] = true; } string contents = JsonConvert.SerializeObject((object)dictionary, JsonSettings); File.WriteAllText(OverridesFilePath, contents); Plugin.Log.LogInfo((object)$"[CharacterVault :: DataStore] Saved {overrides.Count} override(s) to '{OverridesFilePath}'"); } catch (Exception ex) { Plugin.Log.LogError((object)("[CharacterVault :: DataStore] Failed to save overrides: " + ex.Message)); } } } public class NetworkManager : MonoBehaviour { 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 readonly HashSet _handshakeCompleted = new HashSet(); private Dictionary> _incomingChunks = new Dictionary>(); 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(); Object.DontDestroyOnLoad((Object)val); Plugin.Log.LogInfo((object)"[CharacterVault :: Network] NetworkManager initialized."); } public void RegisterRPCs() { ZRoutedRpc.instance.Register("CharacterVault_Handshake", (Action)RPC_Handshake); ZRoutedRpc.instance.Register("CharacterVault_ProfileDataChunk", (Method)RPC_ProfileDataChunk); ZRoutedRpc.instance.Register("CharacterVault_SaveProfileChunk", (Method)RPC_SaveProfileChunk); ZRoutedRpc.instance.Register("CharacterVault_KickReason", (Action)RPC_KickReason); 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()) { ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "CharacterVault_KickReason", new object[1] { reason }); ((MonoBehaviour)this).StartCoroutine(DisconnectRejectedPeer(peer)); } } private IEnumerator DisconnectRejectedPeer(ZNetPeer peer) { yield return null; 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.3.0") { Plugin.Log.LogWarning((object)string.Format("[CharacterVault :: Network] Peer {0} wrong mod version: '{1}' (expected '{2}'). KICKING.", sender, version, "2.3.0")); ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { ZNet.instance.Disconnect(peer); } 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 SteamID " + playerId + " ('" + peer2.m_playerName + "')...")); PlayerSnapshot snapshot = SnapshotManager.GetSnapshot(playerId); byte[] array; if (snapshot != null && snapshot.HasData) { array = snapshot.GetProfileBytes(); Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} (SteamID {playerId}) -> SENDING EXISTING STORED PROFILE ({array.Length} bytes)."); } else { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Peer {sender} (SteamID {playerId}) -> NO STORED SNAPSHOT (First Join). Requesting client-side clean initialization."); array = Array.Empty(); } SendProfileDataToClient(sender, array, snapshot?.IsPlayerData ?? false, snapshot == null); } else if (version == "request") { Plugin.Log.LogInfo((object)"[CharacterVault :: Network] Server requested handshake. Replying with version '2.3.0'."); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "CharacterVault_Handshake", new object[1] { "2.3.0" }); } } public void SendHandshakeRequest(ZNetPeer peer) { Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] Initiating handshake with peer {peer.m_uid} (SteamID {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 WaitForSeconds(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.")); ZNet.instance.Disconnect(peer); } } public void OnPeerDisconnected(long 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) { ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { string playerId = ZNetHelper.GetPlayerId(peer); Plugin.Log.LogInfo((object)$"[CharacterVault :: Network] SERVER: Received full profile upload ({array.Length} bytes) from peer {sender} (SteamID {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(); } if (!_incomingChunks.ContainsKey(sender)) { _incomingChunks[sender] = new Dictionary(); } _incomingChunks[sender][chunkIndex] = chunk; if (_incomingChunks[sender].Count == totalChunks) { List list = new List(); for (int i = 0; i < totalChunks; i++) { list.AddRange(_incomingChunks[sender][i]); } _incomingChunks.Remove(sender); return list.ToArray(); } return null; } } public static class OverrideManager { private static readonly HashSet _memoryOverrides = new HashSet(); public static bool HasOverride(string playerId) { HashSet hashSet = DataStore.LoadOverrides(); if (!_memoryOverrides.Contains(playerId)) { return hashSet.Contains(playerId); } return true; } public static bool ConsumeOverride(string playerId) { HashSet hashSet = DataStore.LoadOverrides(); bool num = hashSet.Remove(playerId); bool flag = _memoryOverrides.Remove(playerId); if (num) { DataStore.SaveOverrides(hashSet); } if (num || flag) { Plugin.Log.LogWarning((object)("[OverrideManager] Consumed override for " + playerId + ". Player allowed through.")); return true; } return false; } public static void GrantMemoryOverride(string playerId) { _memoryOverrides.Add(playerId); Plugin.Log.LogWarning((object)("[OverrideManager] Granted in-memory override for " + playerId + ".")); } public static bool RevokeOverride(string playerId) { return _memoryOverrides.Remove(playerId); } public static IReadOnlyCollection GetMemoryOverrides() { return _memoryOverrides; } } 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 SteamID {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 SteamID {playerId} ('{playerSnapshot.CharacterName}'), created: {playerSnapshot.SnapshotTime}"); } else { Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] NO existing snapshot found for SteamID " + playerId + ".")); } return playerSnapshot; } public static void SaveSnapshot(PlayerSnapshot snapshot) { Plugin.Log.LogInfo((object)("[CharacterVault :: Snapshot] Saving snapshot for SteamID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "')...")); DataStore.SaveSnapshot(snapshot); } } } namespace CharacterVault.Patches { [HarmonyPatch(typeof(Chat), "RPC_ChatMessage")] public static class Chat_RPC_ChatMessage_Patch { [HarmonyPrefix] public static bool Prefix(long sender, Vector3 position, int type, UserInfo userInfo, string text) { try { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return true; } if (!AdminCommandHandler.IsCommand(text)) { return true; } return !AdminCommandHandler.Handle(sender, text); } catch (Exception ex) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogWarning((object)("[ChatPatch] Exception in chat prefix: " + ex.Message)); } 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."); 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()).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; } 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_0029: 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_0030: Expected O, but got Unknown //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Invalid comparison between Unknown and I4 //IL_0051: 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, (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(); } PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); if (playerProfile == null) { return Array.Empty(); } 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(); } public static Type? GetUtilsType() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { Type type = assembly.GetType("Utils"); if (type != null) { return type; } } catch { } } return null; } } [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() .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) { object value = Traverse.Create((object)player).Field(fieldName).GetValue(); value.GetType().GetMethod("Clear").Invoke(value, null); } } [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."); Game.instance.SavePlayerProfile(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 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 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 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(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 (string.IsNullOrWhiteSpace(playerId) || string.IsNullOrWhiteSpace(playerName)) { if (ModConfig.VerboseLogging.Value) { Plugin.Log.LogInfo((object)"[ZNetPatch] Skipping peer with invalid playerId or empty name."); } return; } Plugin.Log.LogInfo((object)("[CharacterVault] Player joining: playerId=" + 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) { string value = ConnectionRejectionManager.ConsumeReason(); if (!string.IsNullOrWhiteSpace(value)) { Traverse.Create((object)__instance).Field("m_connectionFailedError").Property("text", (object[])null) .SetValue((object)value); } } } } namespace CharacterVault.Models { public class CharacterRecord { public string PlayerId { get; set; } public string CharacterName { get; set; } = string.Empty; public DateTime RegisteredAt { get; set; } = DateTime.UtcNow; public DateTime LastSeenAt { get; set; } = DateTime.UtcNow; } public class MismatchReport { public bool InventoryChanged { get; set; } public bool SkillsChanged { get; set; } public bool HasChanges { get { if (!InventoryChanged) { return SkillsChanged; } return true; } } public string Summary { get { List list = new List(); if (InventoryChanged) { list.Add("inventory"); } if (SkillsChanged) { list.Add("skills"); } if (list.Count != 0) { return "Mismatch detected in: " + string.Join(", ", list); } return "No changes detected"; } } public override string ToString() { return Summary; } } public class PlayerSnapshot { public string PlayerId { get; set; } 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(); } } } 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(); } 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(); } 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(); } 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(); } } private static Type? GetUtilsType() { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { Type type = assembly.GetType("Utils"); if (type != null) { return type; } } catch { } } return null; } } internal static class ZNetHelper { private static readonly FieldInfo? FiPeers; private static readonly FieldInfo? FiAdminList; private static readonly MethodInfo? MiListContainsId; static ZNetHelper() { FiPeers = typeof(ZNet).GetField("m_peers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); FiAdminList = typeof(ZNet).GetField("m_adminList", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); MiListContainsId = typeof(ZNet).GetMethod("ListContainsId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (FiPeers == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[ZNetHelper] ZNet.m_peers not found — peer enumeration unavailable."); } } if (FiAdminList == null || MiListContainsId == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)"[ZNetHelper] Admin list fields not found — /sc commands unavailable."); } } } public static List GetPeers() { if ((Object)(object)ZNet.instance == (Object)null || FiPeers == null) { return new List(); } if (!(FiPeers.GetValue(ZNet.instance) is List collection)) { return new List(); } return new List(collection); } public static ZNetPeer? GetPeerByRpc(ZRpc rpc) { return ((IEnumerable)GetPeers()).FirstOrDefault((Func)((ZNetPeer p) => p.m_rpc == rpc)); } public static string GetPlayerId(ZNetPeer peer) { object obj; if (peer == null) { obj = null; } else { ISocket socket = peer.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } if (obj == null) { obj = ""; } return (string)obj; } public static bool IsAdmin(string playerId) { if ((Object)(object)ZNet.instance == (Object)null || FiAdminList == null || MiListContainsId == null) { return false; } try { object value = FiAdminList.GetValue(ZNet.instance); if (value == null) { return false; } return (bool)(MiListContainsId.Invoke(ZNet.instance, new object[2] { value, playerId }) ?? ((object)false)); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)("[ZNetHelper] IsAdmin check failed: " + ex.Message)); } return false; } } } }