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 ServerCharacterVault v1.0.0
BepInEx\plugins\ServerCharacterVault\ServerCharacterVault.dll
Decompiled 13 hours agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using ServerCharacterVault.Helpers; using ServerCharacterVault.Models; using ServerCharacterVault.Patches; using ServerCharacterVault.Systems; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [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 ServerCharacterVault { 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> 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"); VerboseLogging = cfg.Bind<bool>("Debug", "VerboseLogging", false, "Enable extra debug logging to the BepInEx console/log file."); } } [BepInPlugin("com.servercharactervault.valheim", "ServerCharacterVault", "1.0.0")] public class Plugin : BaseUnityPlugin { public const string ModGuid = "com.servercharactervault.valheim"; public const string ModName = "ServerCharacterVault"; public const string ModVersion = "1.0.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)" ServerCharacterVault v1.0.0 loading..."); Log.LogInfo((object)"══════════════════════════════════════════"); ModConfig.Initialize(((BaseUnityPlugin)this).Config); DataStore.Initialize(); BindingManager.Load(); _harmony = new Harmony("com.servercharactervault.valheim"); _harmony.PatchAll(); Log.LogInfo((object)"[ServerCharacterVault] All Harmony patches applied."); Log.LogInfo((object)"[ServerCharacterVault] Loaded successfully. Waiting for network initialization..."); NetworkManager.Initialize(); ClientSyncManager.Initialize(); } catch (Exception arg) { Log.LogError((object)string.Format("[{0}] FATAL: Failed to initialize — {1}", "ServerCharacterVault", arg)); } } private void Start() { } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } Log.LogInfo((object)"[ServerCharacterVault] Unloaded."); } } } namespace ServerCharacterVault.Systems { public static class AdminCommandHandler { private static readonly string[] Prefixes = new string[2] { "/scv", "/sc" }; public static bool IsCommand(string text) { if (string.IsNullOrWhiteSpace(text)) { return false; } string[] prefixes = Prefixes; foreach (string text2 in prefixes) { if (text.StartsWith(text2, StringComparison.OrdinalIgnoreCase) && (text.Length == text2.Length || text[text2.Length] == ' ')) { return true; } } return false; } private static string GetMatchingPrefix(string text) { string[] prefixes = Prefixes; foreach (string text2 in prefixes) { if (text.StartsWith(text2, StringComparison.OrdinalIgnoreCase)) { return text2; } } return "/scv"; } 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 matchingPrefix = GetMatchingPrefix(text); string[] array = text.Substring(matchingPrefix.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 "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 /scv help for a list."); 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; } bool num = DataStore.WipePlayerData(targetId); BindingManager.Load(); if (num) { 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<string, CharacterRecord> 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<string, CharacterRecord> item in all) { stringBuilder.AppendLine($" {item.Key} → '{item.Value.CharacterName}' (since {item.Value.RegisteredAt:yyyy-MM-dd})"); } LogToAdmin(adminPeer, stringBuilder.ToString().TrimEnd(Array.Empty<char>())); 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); StringBuilder stringBuilder = new StringBuilder("Status for " + targetId + ":\n"); stringBuilder.AppendLine(" Binding: " + ((registeredName != null) ? ("'" + registeredName + "'") : "Not registered")); stringBuilder.Append(" Snapshot: " + ((playerSnapshot != null) ? $"Taken {playerSnapshot.SnapshotTime:yyyy-MM-dd HH:mm} UTC" : "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: /scv " + tokens[0] + " Steam_76561198XXXXXXXXX"); return false; } targetId = tokens[index]; if (ZNetHelper.IsValidPlayerId(targetId)) { return true; } LogToAdmin(adminPeer, "Invalid player ID. Use the platform ID shown in /scv list, such as Steam_... or Xbox_...."); return false; } 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, "ServerCharacterVault", 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 "ServerCharacterVault Admin Commands:\n /scv remove [playerId] — Remove character binding (allows re-register)\n /scv wipe [playerId] — Delete ALL server data for player (blank slate next join)\n /scv list — Show all bindings\n /scv status [playerId] — Show binding + snapshot info\n /scv help — This message\n (Note: /sc prefix is also supported)"; } } 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) { if (!_bindings.Remove(playerId)) { return false; } Save(); Plugin.Log.LogInfo((object)("[BindingManager] Removed binding for " + playerId)); return true; } public static IReadOnlyDictionary<string, CharacterRecord> GetAll() { return new Dictionary<string, CharacterRecord>(_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("ServerCharacterVault_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); } } 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"); private static string SnapshotPath(string playerId) { return Path.Combine(_snapshotsDir, playerId + ".json"); } public static void Initialize() { _rootDir = Path.Combine(Paths.ConfigPath, "ServerCharacterVault"); _snapshotsDir = Path.Combine(_rootDir, "snapshots"); Directory.CreateDirectory(_rootDir); Directory.CreateDirectory(_snapshotsDir); Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Root Data Directory: " + _rootDir)); Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Snapshots Directory: " + _snapshotsDir)); } public static Dictionary<string, CharacterRecord> LoadBindings() { try { if (!File.Exists(BindingsFilePath)) { return new Dictionary<string, CharacterRecord>(); } return JsonConvert.DeserializeObject<Dictionary<string, CharacterRecord>>(File.ReadAllText(BindingsFilePath), JsonSettings) ?? new Dictionary<string, CharacterRecord>(); } catch (Exception ex) { Plugin.Log.LogError((object)("[ServerCharacterVault :: 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 = JsonConvert.SerializeObject((object)bindings, JsonSettings); WriteAllTextAtomically(BindingsFilePath, content); Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: DataStore] Saved {bindings.Count} binding(s) to '{BindingsFilePath}'"); } catch (Exception ex) { Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)("[ServerCharacterVault :: DataStore] No snapshot file found at '" + text + "'")); return null; } PlayerSnapshot result = JsonConvert.DeserializeObject<PlayerSnapshot>(File.ReadAllText(text), JsonSettings); Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Loaded snapshot file for platform ID " + playerId + " from '" + text + "'")); return result; } catch (Exception ex) { Plugin.Log.LogError((object)("[ServerCharacterVault :: 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 = JsonConvert.SerializeObject((object)snapshot, JsonSettings); WriteAllTextAtomically(text, content); Plugin.Log.LogInfo((object)("[ServerCharacterVault :: DataStore] Saved snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "') -> '" + text + "'")); } catch (Exception ex) { Plugin.Log.LogError((object)("[ServerCharacterVault :: DataStore] Failed to save snapshot for platform ID " + snapshot.PlayerId + ": " + ex.Message)); } } 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)("[ServerCharacterVault :: DataStore] Wipe: deleted snapshot file for platform ID " + playerId + ".")); result = true; } catch (Exception ex) { Plugin.Log.LogError((object)("[ServerCharacterVault :: DataStore] WIPE FAILED: Could not delete snapshot for " + playerId + ": " + ex.Message)); } } Dictionary<string, CharacterRecord> dictionary = LoadBindings(); if (dictionary.ContainsKey(playerId)) { dictionary.Remove(playerId); SaveBindings(dictionary); Plugin.Log.LogInfo((object)("[ServerCharacterVault :: 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 = "ServerCharacterVault_Handshake"; private const string RpcSaveProfileChunk = "ServerCharacterVault_SaveProfileChunk"; private const string RpcProfileDataChunk = "ServerCharacterVault_ProfileDataChunk"; private const string RpcKickReason = "ServerCharacterVault_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("ServerCharacterVault_NetworkManager"); Instance = val.AddComponent<NetworkManager>(); Object.DontDestroyOnLoad((Object)val); Plugin.Log.LogInfo((object)"[ServerCharacterVault :: Network] NetworkManager initialized."); } public void RegisterRPCs() { ZRoutedRpc.instance.Register<string>("ServerCharacterVault_Handshake", (Action<long, string>)RPC_Handshake); ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("ServerCharacterVault_ProfileDataChunk", (Method<int, int, bool, bool, ZPackage>)RPC_ProfileDataChunk); ZRoutedRpc.instance.Register<int, int, bool, bool, ZPackage>("ServerCharacterVault_SaveProfileChunk", (Method<int, int, bool, bool, ZPackage>)RPC_SaveProfileChunk); ZRoutedRpc.instance.Register<string>("ServerCharacterVault_KickReason", (Action<long, string>)RPC_KickReason); Plugin.Log.LogInfo((object)"[ServerCharacterVault :: 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)("[ServerCharacterVault :: 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, "ServerCharacterVault_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 != "1.0.0") { Plugin.Log.LogWarning((object)string.Format("[ServerCharacterVault :: Network] Peer {0} wrong mod version: '{1}' (expected '{2}'). KICKING.", sender, version, "1.0.0")); ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer != null) { ZNet.instance.Disconnect(peer); } return; } Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Peer {sender} COMPLETED HANDSHAKE (v{version})."); _handshakeCompleted.Add(sender); ZNetPeer peer2 = ZNet.instance.GetPeer(sender); if (peer2 == null) { Plugin.Log.LogWarning((object)$"[ServerCharacterVault :: Network] Peer {sender} null after handshake!"); return; } string playerId = ZNetHelper.GetPlayerId(peer2); Plugin.Log.LogInfo((object)("[ServerCharacterVault :: Network] Checking snapshot store for platform ID " + playerId + " ('" + peer2.m_playerName + "')...")); PlayerSnapshot snapshot = SnapshotManager.GetSnapshot(playerId); byte[] array; if (snapshot != null && snapshot.HasData) { array = snapshot.GetProfileBytes(); Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> sending existing stored profile ({array.Length} bytes)."); } else { Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Peer {sender} (platform ID {playerId}) -> no stored snapshot (first join). Requesting client-side clean initialization."); array = Array.Empty<byte>(); } SendProfileDataToClient(sender, array, snapshot?.IsPlayerData ?? false, snapshot == null); } else if (version == "request") { Plugin.Log.LogInfo((object)"[ServerCharacterVault :: Network] Server requested handshake. Replying with version '1.0.0'."); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "ServerCharacterVault_Handshake", new object[1] { "1.0.0" }); } } public void SendHandshakeRequest(ZNetPeer peer) { Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Initiating handshake with peer {peer.m_uid} (platform ID {peer.m_socket.GetHostName()})..."); ZRoutedRpc.instance.InvokeRoutedRPC(peer.m_uid, "ServerCharacterVault_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)($"[ServerCharacterVault :: 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) { _incomingChunks.Remove(peerId); if (_handshakeCompleted.Remove(peerId)) { Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Cleaned up handshake tracking for disconnected peer {peerId}."); } } private void SendProfileDataToClient(long peerId, byte[] data, bool isPlayerData, bool isFirstJoin) { Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Transmitting {data.Length} profile bytes to client peer {peerId}..."); SendChunks(peerId, "ServerCharacterVault_ProfileDataChunk", data, isPlayerData, isFirstJoin); } public void SendProfileDataToServer(byte[] data, bool isPlayerData = false) { Plugin.Log.LogInfo((object)$"[ServerCharacterVault :: Network] Transmitting {data.Length} profile bytes to server..."); SendChunks(0L, "ServerCharacterVault_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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: 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)$"[ServerCharacterVault :: Snapshot] Found existing snapshot for platform ID {playerId} ('{playerSnapshot.CharacterName}'), created: {playerSnapshot.SnapshotTime}"); } else { Plugin.Log.LogInfo((object)("[ServerCharacterVault :: Snapshot] No existing snapshot found for platform ID " + playerId + ".")); } return playerSnapshot; } public static void SaveSnapshot(PlayerSnapshot snapshot) { Plugin.Log.LogInfo((object)("[ServerCharacterVault :: Snapshot] Saving snapshot for platform ID " + snapshot.PlayerId + " ('" + snapshot.CharacterName + "')...")); DataStore.SaveSnapshot(snapshot); } } } namespace ServerCharacterVault.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<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; } 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<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."); 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<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(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)("[ServerCharacterVault] 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)("[ServerCharacterVault] 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)$"[ServerCharacterVault] 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 ServerCharacterVault.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 ServerCharacterVault.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)"[ServerCharacterVault :: ProfileHelper] Could not find PlayerProfile.SavePlayerToDisk method via reflection."); } if (_playerNameField == null || _filenameField == null) { Plugin.Log.LogWarning((object)"[ServerCharacterVault :: ProfileHelper] Could not find name/filename fields on PlayerProfile."); } } catch (Exception ex) { Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)("[ServerCharacterVault :: ProfileHelper] Generating native blank .fch profile for character '" + characterName + "'...")); EnsureInitialized(); if (_savePlayerToDiskMethod == null) { Plugin.Log.LogError((object)"[ServerCharacterVault :: 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)("[ServerCharacterVault :: ProfileHelper] Activator.CreateInstance failed: " + ex.Message)); } } if (val == null) { Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)$"[ServerCharacterVault :: ProfileHelper] SUCCESS: Generated {array.Length}-byte native blank profile for '{characterName}'."); return array; } Plugin.Log.LogError((object)("[ServerCharacterVault :: 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)("[ServerCharacterVault :: 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)$"[ServerCharacterVault :: ProfileHelper] Failed to generate blank profile for '{characterName}': {ex2}"); } return Array.Empty<byte>(); } } } 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 — admin commands unavailable."); } } } 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 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 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; } } if (!playerId.StartsWith("Steam_", StringComparison.Ordinal)) { return playerId.StartsWith("Xbox_", StringComparison.Ordinal); } return true; } 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; } } } }