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 WorldBoundInventory v1.1.0
IndividualInventory.dll
Decompiled 3 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("IndividualInventory")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("IndividualInventory")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("2d915084-84f2-40c1-8624-cf0659d0e0d4")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace FandrayMods.WorldBoundInventory; [BepInPlugin("fandray.valheim.worldboundinventory", "WorldBoundInventory", "1.1.0")] public sealed class WorldBoundInventoryPlugin : BaseUnityPlugin { public const string PluginGuid = "fandray.valheim.worldboundinventory"; public const string PluginName = "WorldBoundInventory"; public const string PluginVersion = "1.1.0"; internal static ManualLogSource Log; internal static ConfigEntry<bool> ModEnabled; internal static ConfigEntry<bool> ImportInventoryIntoFirstWorld; internal static ConfigEntry<bool> EmptyInventoryInNewWorlds; internal static ConfigEntry<float> AutosaveSeconds; internal static ConfigEntry<bool> KeepBackup; internal static ConfigEntry<bool> EnforceModOnServer; internal static ConfigEntry<bool> RequireExactAssembly; internal static ConfigEntry<float> HandshakeTimeoutSeconds; private Harmony _harmony; private void Awake() { //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Expected O, but got Unknown //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Expected O, but got Unknown //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_0170: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ModEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable separate character inventory for every world/server."); ImportInventoryIntoFirstWorld = ((BaseUnityPlugin)this).Config.Bind<bool>("Migration", "ImportInventoryIntoFirstWorld", true, "On the first launch with this mod, assign the character's current inventory to the first world entered."); EmptyInventoryInNewWorlds = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EmptyInventoryInNewWorlds", true, "When the character enters an unknown world, create an empty inventory instead of copying the last vanilla inventory."); AutosaveSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Saving", "AutosaveSeconds", 15f, new ConfigDescription("Delay between inventory autosaves. Vanilla profile saves and game exit also save immediately.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 300f), Array.Empty<object>())); KeepBackup = ((BaseUnityPlugin)this).Config.Bind<bool>("Saving", "KeepBackup", true, "Keep the previous valid slot as a .bak file."); EnforceModOnServer = ((BaseUnityPlugin)this).Config.Bind<bool>("Server enforcement", "EnforceModOnServer", true, "When this instance is the host/server, reject clients that do not have this mod."); RequireExactAssembly = ((BaseUnityPlugin)this).Config.Bind<bool>("Server enforcement", "RequireExactAssembly", true, "Require clients to use the exact same WorldBoundInventory.dll as the server (SHA-256 check)."); HandshakeTimeoutSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Server enforcement", "HandshakeTimeoutSeconds", 8f, new ConfigDescription("Maximum time allowed for the client mod handshake.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(3f, 30f), Array.Empty<object>())); WorldInventoryStore.Initialize(Path.Combine(Paths.ConfigPath, "WorldBoundInventory", "Inventories")); _harmony = new Harmony("fandray.valheim.worldboundinventory"); if (!WorldInventoryPatches.Apply(_harmony)) { ((BaseUnityPlugin)this).Logger.LogError((object)"Required PlayerProfile methods were not found. The mod has been disabled to protect the inventory."); ModEnabled.Value = false; } else if (!ServerHandshakeGate.Apply(_harmony)) { ((BaseUnityPlugin)this).Logger.LogError((object)"Required ZNet handshake methods were not found. The mod has been disabled to avoid unprotected connections."); ModEnabled.Value = false; _harmony.UnpatchSelf(); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)"WorldBoundInventory 1.1.0 loaded."); } } private void Update() { if (ModEnabled != null && ModEnabled.Value) { WorldInventoryStore.Tick(Time.unscaledTime); ServerHandshakeGate.Tick(Time.unscaledTime); } } private void OnApplicationQuit() { WorldInventoryStore.SaveActive("application quit", force: true); } private void OnDestroy() { WorldInventoryStore.SaveActive("plugin shutdown", force: true); ServerHandshakeGate.Reset(); if (_harmony != null) { _harmony.UnpatchSelf(); } } } internal static class WorldInventoryPatches { private static MethodInfo FindPlayerMethod(string name) { return AccessTools.GetDeclaredMethods(typeof(PlayerProfile)).FirstOrDefault((MethodInfo method) => method.Name == name && method.GetParameters().Any((ParameterInfo parameter) => typeof(Player).IsAssignableFrom(parameter.ParameterType))); } internal static bool Apply(Harmony harmony) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Expected O, but got Unknown MethodInfo methodInfo = FindPlayerMethod("LoadPlayerData"); MethodInfo methodInfo2 = FindPlayerMethod("SavePlayerData"); if (methodInfo == null || methodInfo2 == null) { return false; } harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(WorldInventoryPatches), "AfterLoadPlayerData", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(WorldInventoryPatches), "BeforeSavePlayerData", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); MethodInfo methodInfo3 = AccessTools.Method(typeof(Inventory), "Changed", Type.EmptyTypes, (Type[])null); if (methodInfo3 != null) { harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(WorldInventoryPatches), "AfterInventoryChanged", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); WorldInventoryStore.InventoryChangedPatchInstalled = true; } else { WorldBoundInventoryPlugin.Log.LogWarning((object)"Inventory.Changed was not found. Timed saving will still be used."); } return true; } public static void AfterLoadPlayerData(PlayerProfile __instance, object[] __args) { if (WorldBoundInventoryPlugin.ModEnabled.Value) { Player val = FindPlayer(__args); if ((Object)(object)val != (Object)null) { WorldInventoryStore.OnVanillaInventoryLoaded(__instance, val); } } } public static void BeforeSavePlayerData(PlayerProfile __instance, object[] __args) { if (WorldBoundInventoryPlugin.ModEnabled.Value) { Player val = FindPlayer(__args); if ((Object)(object)val != (Object)null) { WorldInventoryStore.SaveForProfile(__instance, val, "vanilla profile save"); } } } public static void AfterInventoryChanged(Inventory __instance) { WorldInventoryStore.MarkDirty(__instance); } private static Player FindPlayer(object[] args) { if (args == null) { return null; } foreach (object obj in args) { Player val = (Player)((obj is Player) ? obj : null); if ((Object)(object)val != (Object)null) { return val; } } return null; } } internal static class WorldInventoryStore { private enum SlotLoadResult { Missing, Loaded, Corrupt } private const string FileMagic = "WBI_SLOT"; private const int FileVersion = 1; private const int MaximumPayloadBytes = 67108864; private static readonly object Sync = new object(); private static string _rootDirectory; private static PlayerProfile _pendingProfile; private static Player _pendingPlayer; private static PlayerProfile _activeProfile; private static Player _activePlayer; private static Inventory _activeInventory; private static string _activeCharacterId; private static string _activeWorldId; private static string _activeWorldName; private static string _activeSlotPath; private static bool _dirty; private static bool _blocked; private static bool _switching; private static bool _preserveBackupOnNextSave; private static float _nextAutosaveTime; internal static bool InventoryChangedPatchInstalled { get; set; } internal static void Initialize(string rootDirectory) { _rootDirectory = rootDirectory; Directory.CreateDirectory(_rootDirectory); } internal static void OnVanillaInventoryLoaded(PlayerProfile profile, Player player) { lock (Sync) { _pendingProfile = profile; _pendingPlayer = player; } TryActivatePending(); } internal static void Tick(float now) { TryActivatePending(); lock (Sync) { if (!((Object)(object)_activePlayer == (Object)null) && _activeInventory != null && !_blocked && !_switching && !(now < _nextAutosaveTime)) { _nextAutosaveTime = now + GetAutosaveInterval(); if (_dirty || !InventoryChangedPatchInstalled) { SaveActiveLocked("timed autosave", force: false); } } } } internal static void MarkDirty(Inventory inventory) { if (!WorldBoundInventoryPlugin.ModEnabled.Value) { return; } lock (Sync) { if (!_switching && !_blocked && inventory != null && inventory == _activeInventory) { _dirty = true; } } } internal static void SaveForProfile(PlayerProfile profile, Player player, string reason) { lock (Sync) { if (!_blocked && !_switching && !((Object)(object)_activePlayer == (Object)null) && profile == _activeProfile && player == _activePlayer) { SaveActiveLocked(reason, force: true); } } } internal static void SaveActive(string reason, bool force) { if (WorldBoundInventoryPlugin.ModEnabled == null || !WorldBoundInventoryPlugin.ModEnabled.Value) { return; } lock (Sync) { SaveActiveLocked(reason, force); } } private static void TryActivatePending() { PlayerProfile pendingProfile; Player pendingPlayer; lock (Sync) { pendingProfile = _pendingProfile; pendingPlayer = _pendingPlayer; } if (pendingProfile == null || (Object)(object)pendingPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null || !WorldIdentity.TryGet(out var worldId, out var worldName)) { return; } string stableId = CharacterIdentity.GetStableId(pendingProfile, pendingPlayer); if (string.IsNullOrEmpty(stableId)) { WorldBoundInventoryPlugin.Log.LogError((object)"Could not determine a stable character ID. Inventory switching was cancelled for safety."); lock (Sync) { _pendingProfile = null; _pendingPlayer = null; return; } } lock (Sync) { if (pendingPlayer == _activePlayer && string.Equals(stableId, _activeCharacterId, StringComparison.Ordinal) && string.Equals(worldId, _activeWorldId, StringComparison.Ordinal)) { _pendingProfile = null; _pendingPlayer = null; return; } if ((Object)(object)_activePlayer != (Object)null && !_blocked) { SaveActiveLocked("before world switch", force: true); } ActivateLocked(pendingProfile, pendingPlayer, stableId, worldId, worldName); _pendingProfile = null; _pendingPlayer = null; } } private static void ActivateLocked(PlayerProfile profile, Player player, string characterId, string worldId, string worldName) { Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory == null) { WorldBoundInventoryPlugin.Log.LogError((object)"Player inventory is unavailable; switching was cancelled."); return; } byte[] payload = SerializeInventory(inventory); string characterDirectory = GetCharacterDirectory(characterId); Directory.CreateDirectory(characterDirectory); string text = Path.Combine(characterDirectory, "world_" + HashForPath(worldId) + ".wbi"); _switching = true; _blocked = false; try { byte[] payload2; string source; switch (TryLoadSlot(text, characterId, worldId, out payload2, out source)) { case SlotLoadResult.Loaded: ReplaceInventory(player, inventory, payload2); ActivateSession(profile, player, inventory, characterId, worldId, worldName, text); _preserveBackupOnNextSave = string.Equals(source, "the backup slot", StringComparison.Ordinal); WorldBoundInventoryPlugin.Log.LogInfo((object)("Loaded inventory for world '" + worldName + "' from " + source + ".")); return; case SlotLoadResult.Corrupt: _blocked = true; ClearActiveSession(); WorldBoundInventoryPlugin.Log.LogError((object)("The inventory slot and its backup could not be read: " + text + ". The vanilla inventory was left untouched and this slot will NOT be overwritten.")); return; } bool flag = (!Directory.EnumerateFiles(characterDirectory, "world_*.wbi*", SearchOption.TopDirectoryOnly).Any() && WorldBoundInventoryPlugin.ImportInventoryIntoFirstWorld.Value) || !WorldBoundInventoryPlugin.EmptyInventoryInNewWorlds.Value; if (!flag) { ReplaceInventory(player, inventory, new byte[0]); } ActivateSession(profile, player, inventory, characterId, worldId, worldName, text); _switching = false; SaveActiveLocked(flag ? "create first/copied world slot" : "create empty world slot", force: true); WorldBoundInventoryPlugin.Log.LogInfo((object)("Created " + (flag ? "an imported" : "an empty") + " inventory for world '" + worldName + "'.")); } catch (Exception ex) { try { ReplaceInventory(player, inventory, payload); } catch (Exception ex2) { WorldBoundInventoryPlugin.Log.LogError((object)("Restoring the pre-switch inventory also failed: " + ex2)); } _blocked = true; ClearActiveSession(); WorldBoundInventoryPlugin.Log.LogError((object)("Inventory switching failed. The vanilla inventory was left as safely as possible. " + ex)); } finally { _switching = false; } } private static void ActivateSession(PlayerProfile profile, Player player, Inventory inventory, string characterId, string worldId, string worldName, string slotPath) { _activeProfile = profile; _activePlayer = player; _activeInventory = inventory; _activeCharacterId = characterId; _activeWorldId = worldId; _activeWorldName = worldName; _activeSlotPath = slotPath; _dirty = true; _blocked = false; _preserveBackupOnNextSave = false; _nextAutosaveTime = Time.unscaledTime + GetAutosaveInterval(); } private static void ClearActiveSession() { _activeProfile = null; _activePlayer = null; _activeInventory = null; _activeCharacterId = null; _activeWorldId = null; _activeWorldName = null; _activeSlotPath = null; _dirty = false; _preserveBackupOnNextSave = false; } private static float GetAutosaveInterval() { if (WorldBoundInventoryPlugin.AutosaveSeconds == null) { return 15f; } return Mathf.Clamp(WorldBoundInventoryPlugin.AutosaveSeconds.Value, 5f, 300f); } private static void SaveActiveLocked(string reason, bool force) { if ((Object)(object)_activePlayer == (Object)null || _activeInventory == null || _blocked || _switching || (!force && !_dirty && InventoryChangedPatchInstalled)) { return; } try { byte[] payload = SerializeInventory(_activeInventory); byte[] data = BuildSlotFile(_activeCharacterId, _activeWorldId, _activeWorldName, payload); if (_preserveBackupOnNextSave && File.Exists(_activeSlotPath + ".bak")) { File.Copy(_activeSlotPath + ".bak", _activeSlotPath, overwrite: true); } WriteAtomically(_activeSlotPath, data); _preserveBackupOnNextSave = false; _dirty = false; WorldBoundInventoryPlugin.Log.LogDebug((object)("Inventory saved (" + reason + ") for world '" + _activeWorldName + "'.")); } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogError((object)("Could not save world inventory (" + reason + "): " + ex)); } } private static byte[] SerializeInventory(Inventory inventory) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown ZPackage val = new ZPackage(); inventory.Save(val); return val.GetArray(); } private static void ReplaceInventory(Player player, Inventory inventory, byte[] payload) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown ((Humanoid)player).UnequipAllItems(); inventory.RemoveAll(); if (payload != null && payload.Length != 0) { inventory.Load(new ZPackage(payload)); } MethodInfo methodInfo = AccessTools.Method(((object)player).GetType(), "SetupEquipment", Type.EmptyTypes, (Type[])null) ?? AccessTools.Method(typeof(Humanoid), "SetupEquipment", Type.EmptyTypes, (Type[])null); if (methodInfo != null) { methodInfo.Invoke(player, null); } MethodInfo methodInfo2 = AccessTools.Method(typeof(Inventory), "Changed", Type.EmptyTypes, (Type[])null); if (methodInfo2 != null) { methodInfo2.Invoke(inventory, null); } } private static SlotLoadResult TryLoadSlot(string slotPath, string expectedCharacterId, string expectedWorldId, out byte[] payload, out string source) { payload = null; source = null; bool flag = File.Exists(slotPath); bool flag2 = File.Exists(slotPath + ".bak"); if (!flag && !flag2) { return SlotLoadResult.Missing; } string error = null; if (flag && TryReadSlotFile(slotPath, expectedCharacterId, expectedWorldId, out payload, out error)) { source = "the main slot"; return SlotLoadResult.Loaded; } if (flag) { WorldBoundInventoryPlugin.Log.LogWarning((object)("Main inventory slot is invalid: " + error)); } if (flag2 && TryReadSlotFile(slotPath + ".bak", expectedCharacterId, expectedWorldId, out payload, out error)) { source = "the backup slot"; WorldBoundInventoryPlugin.Log.LogWarning((object)"The backup inventory was loaded because the main slot was invalid."); return SlotLoadResult.Loaded; } if (flag2) { WorldBoundInventoryPlugin.Log.LogWarning((object)("Backup inventory slot is invalid: " + error)); } return SlotLoadResult.Corrupt; } private static byte[] BuildSlotFile(string characterId, string worldId, string worldName, byte[] payload) { byte[] array = ComputeHash(payload); using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8); binaryWriter.Write("WBI_SLOT"); binaryWriter.Write(1); binaryWriter.Write(characterId ?? string.Empty); binaryWriter.Write(worldId ?? string.Empty); binaryWriter.Write(worldName ?? string.Empty); binaryWriter.Write(DateTime.UtcNow.Ticks); binaryWriter.Write(payload.Length); binaryWriter.Write(payload); binaryWriter.Write(array.Length); binaryWriter.Write(array); binaryWriter.Flush(); return memoryStream.ToArray(); } private static bool TryReadSlotFile(string path, string expectedCharacterId, string expectedWorldId, out byte[] payload, out string error) { payload = null; error = null; try { using FileStream input = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using BinaryReader binaryReader = new BinaryReader(input, Encoding.UTF8); string a = binaryReader.ReadString(); int num = binaryReader.ReadInt32(); string a2 = binaryReader.ReadString(); string a3 = binaryReader.ReadString(); binaryReader.ReadString(); binaryReader.ReadInt64(); if (!string.Equals(a, "WBI_SLOT", StringComparison.Ordinal) || num != 1) { error = "unknown file format/version"; return false; } if (!string.Equals(a2, expectedCharacterId, StringComparison.Ordinal) || !string.Equals(a3, expectedWorldId, StringComparison.Ordinal)) { error = "slot identity does not match this character/world"; return false; } int num2 = binaryReader.ReadInt32(); if (num2 < 0 || num2 > 67108864) { error = "invalid inventory payload size"; return false; } byte[] array = binaryReader.ReadBytes(num2); if (array.Length != num2) { error = "inventory payload is truncated"; return false; } int num3 = binaryReader.ReadInt32(); if (num3 != 32) { error = "invalid checksum size"; return false; } byte[] array2 = binaryReader.ReadBytes(num3); if (array2.Length != num3 || !FixedTimeEquals(array2, ComputeHash(array))) { error = "checksum mismatch"; return false; } payload = array; return true; } catch (Exception ex) { error = ex.GetType().Name + ": " + ex.Message; return false; } } private static bool FixedTimeEquals(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } private static byte[] ComputeHash(byte[] data) { using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(data ?? new byte[0]); } private static void WriteAtomically(string path, byte[] data) { string directoryName = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(directoryName)) { throw new InvalidOperationException("Inventory slot has no parent directory."); } Directory.CreateDirectory(directoryName); string text = path + ".tmp"; string text2 = path + ".bak"; try { using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { fileStream.Write(data, 0, data.Length); fileStream.Flush(flushToDisk: true); } if (!File.Exists(path)) { File.Move(text, path); return; } try { File.Replace(text, path, text2, ignoreMetadataErrors: true); } catch (PlatformNotSupportedException) { FallbackReplace(text, path, text2); } catch (IOException) { FallbackReplace(text, path, text2); } if (!WorldBoundInventoryPlugin.KeepBackup.Value && File.Exists(text2)) { File.Delete(text2); } } finally { if (File.Exists(text)) { File.Delete(text); } } } private static void FallbackReplace(string temporaryPath, string path, string backupPath) { File.Copy(path, backupPath, overwrite: true); File.Copy(temporaryPath, path, overwrite: true); File.Delete(temporaryPath); } private static string GetCharacterDirectory(string characterId) { return Path.Combine(_rootDirectory, "character_" + HashForPath(characterId)); } private static string HashForPath(string value) { byte[] bytes = Encoding.UTF8.GetBytes(value ?? string.Empty); byte[] array = ComputeHash(bytes); StringBuilder stringBuilder = new StringBuilder(24); for (int i = 0; i < 12; i++) { stringBuilder.Append(array[i].ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } } internal static class CharacterIdentity { internal static string GetStableId(PlayerProfile profile, Player player) { object value = ReflectionValue.TryInvoke(profile, "GetPlayerID"); string text = NormalizeNumber(value); if (!string.IsNullOrEmpty(text) && text != "0") { return "player-id:" + text; } object obj = ReflectionValue.TryInvoke(profile, "GetFilename"); string text2 = obj as string; if (!string.IsNullOrWhiteSpace(text2)) { return "profile-file:" + text2.Trim(); } object obj2 = ReflectionValue.TryGetField(profile, "m_filename", "m_fileName"); text2 = obj2 as string; if (!string.IsNullOrWhiteSpace(text2)) { return "profile-file:" + text2.Trim(); } object obj3 = ReflectionValue.TryInvoke(profile, "GetPlayerName"); string text3 = obj3 as string; if (string.IsNullOrWhiteSpace(text3) && (Object)(object)player != (Object)null) { text3 = player.GetPlayerName(); } return string.IsNullOrWhiteSpace(text3) ? null : ("player-name-fallback:" + text3.Trim()); } private static string NormalizeNumber(object value) { if (value == null) { return null; } return (value is IFormattable formattable) ? formattable.ToString(null, CultureInfo.InvariantCulture) : value.ToString(); } } internal static class WorldIdentity { internal static bool TryGet(out string worldId, out string worldName) { worldId = null; worldName = "unknown"; ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return false; } object obj = ReflectionValue.TryInvoke(instance, "GetWorldName"); string text = obj as string; if (!string.IsNullOrWhiteSpace(text)) { worldName = text.Trim(); } object value = ReflectionValue.TryInvoke(instance, "GetWorldUID"); string text2 = NormalizeUid(value); if (string.IsNullOrEmpty(text2) || text2 == "0") { object instance2 = ReflectionValue.TryGetField(instance, "m_world"); object value2 = ReflectionValue.TryGetField(instance2, "m_uid", "m_worldUID", "m_worldUid"); text2 = NormalizeUid(value2); } if (string.IsNullOrEmpty(text2) || text2 == "0") { return false; } worldId = "world-uid:" + text2; return true; } private static string NormalizeUid(object value) { if (value == null) { return null; } return (value is IFormattable formattable) ? formattable.ToString(null, CultureInfo.InvariantCulture) : value.ToString(); } } internal static class ReflectionValue { internal static object TryInvoke(object instance, string methodName) { if (instance == null) { return null; } try { MethodInfo methodInfo = AccessTools.Method(instance.GetType(), methodName, Type.EmptyTypes, (Type[])null); return (methodInfo == null) ? null : methodInfo.Invoke(instance, null); } catch { return null; } } internal static object TryGetField(object instance, params string[] fieldNames) { if (instance == null || fieldNames == null) { return null; } for (int i = 0; i < fieldNames.Length; i++) { try { FieldInfo fieldInfo = AccessTools.Field(instance.GetType(), fieldNames[i]); if (fieldInfo != null) { return fieldInfo.GetValue(instance); } } catch { } } return null; } } internal static class ServerHandshakeGate { private sealed class PeerHandshake { internal float Deadline; internal float HeartbeatDeadline; internal bool Approved; internal bool Rejected; internal bool DisconnectSent; internal float SoftDisconnectAt; internal float HardDisconnectAt; internal string Reason; } private const int ProtocolVersion = 1; private const string ClientHelloRpc = "WBI_ClientHello_V1"; private const string ClientHeartbeatRpc = "WBI_ClientHeartbeat_V1"; private const string ServerAcceptedRpc = "WBI_ServerAccepted_V1"; private const string ServerDeniedRpc = "WBI_ServerDenied_V1"; private static readonly object Sync = new object(); private static readonly Dictionary<ZRpc, PeerHandshake> Peers = new Dictionary<ZRpc, PeerHandshake>(); private static MethodInfo _getPeerByRpc; private static MethodInfo _disconnectPeer; private static string _assemblySha256; private static bool _serverConfirmed; private static ZRpc _serverRpc; private static float _nextHeartbeatTime; internal static bool Apply(Harmony harmony) { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Expected O, but got Unknown //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.GetDeclaredMethods(typeof(ZNet)).FirstOrDefault((MethodInfo method) => method.Name == "OnNewConnection" && method.GetParameters().Length == 1 && typeof(ZNetPeer).IsAssignableFrom(method.GetParameters()[0].ParameterType)); MethodInfo methodInfo2 = AccessTools.GetDeclaredMethods(typeof(ZNet)).FirstOrDefault(delegate(MethodInfo method) { if (method.Name != "RPC_PeerInfo") { return false; } ParameterInfo[] parameters = method.GetParameters(); return parameters.Length >= 2 && typeof(ZRpc).IsAssignableFrom(parameters[0].ParameterType) && typeof(ZPackage).IsAssignableFrom(parameters[1].ParameterType); }); if (methodInfo == null || methodInfo2 == null) { return false; } CacheNetworkMethods(); _assemblySha256 = CalculateOwnAssemblyHash(); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(ServerHandshakeGate), "AfterNewConnection", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeof(ServerHandshakeGate), "BeforePeerInfo", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); WorldBoundInventoryPlugin.Log.LogInfo((object)("Server enforcement ready. Assembly SHA-256: " + ShortHash(_assemblySha256))); return true; } public static void AfterNewConnection(ZNet __instance, ZNetPeer __0) { if (!WorldBoundInventoryPlugin.ModEnabled.Value || __0 == null || __0.m_rpc == null) { return; } ZRpc rpc = __0.m_rpc; try { rpc.Register<ZPackage>("WBI_ClientHello_V1", (Action<ZRpc, ZPackage>)OnClientHello); rpc.Register<ZPackage>("WBI_ClientHeartbeat_V1", (Action<ZRpc, ZPackage>)OnClientHeartbeat); rpc.Register<ZPackage>("WBI_ServerAccepted_V1", (Action<ZRpc, ZPackage>)OnServerAccepted); rpc.Register<ZPackage>("WBI_ServerDenied_V1", (Action<ZRpc, ZPackage>)OnServerDenied); if (__instance.IsServer() && IsEnforcementEnabled()) { lock (Sync) { if (!Peers.TryGetValue(rpc, out var value)) { value = new PeerHandshake(); Peers[rpc] = value; } value.Deadline = Time.unscaledTime + GetTimeout(); return; } } _serverConfirmed = false; _serverRpc = rpc; SendClientHello(rpc); } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogError((object)("Could not initialize the mandatory-mod handshake: " + ex)); if (__instance.IsServer() && IsEnforcementEnabled()) { Reject(rpc, "handshake initialization failed"); } } } public static bool BeforePeerInfo(ZNet __instance, ZRpc __0) { if (!WorldBoundInventoryPlugin.ModEnabled.Value || !__instance.IsServer() || !IsEnforcementEnabled()) { return true; } lock (Sync) { if (__0 != null && Peers.TryGetValue(__0, out var value) && value.Approved) { return true; } } Reject(__0, "WorldBoundInventory is missing or incompatible"); return false; } internal static void Tick(float now) { if ((Object)(object)ZNet.instance == (Object)null) { return; } if (!ZNet.instance.IsServer()) { if (_serverConfirmed && _serverRpc != null && now >= _nextHeartbeatTime) { _nextHeartbeatTime = now + 3f; SendClientHeartbeat(_serverRpc); } return; } if (!IsEnforcementEnabled()) { lock (Sync) { Peers.Clear(); return; } } List<ZRpc> list = new List<ZRpc>(); List<ZRpc> list2 = new List<ZRpc>(); List<ZRpc> list3 = new List<ZRpc>(); List<ZRpc> list4 = new List<ZRpc>(); lock (Sync) { foreach (KeyValuePair<ZRpc, PeerHandshake> peer in Peers) { ZRpc key = peer.Key; PeerHandshake value = peer.Value; if (!IsRpcConnected(key)) { list4.Add(key); continue; } if (!value.Approved && !value.Rejected && now >= value.Deadline) { list.Add(key); } if (value.Approved && !value.Rejected && now >= value.HeartbeatDeadline) { list.Add(key); } if (value.Rejected && !value.DisconnectSent && now >= value.SoftDisconnectAt) { value.DisconnectSent = true; list2.Add(key); } if (value.Rejected && now >= value.HardDisconnectAt) { list3.Add(key); } } for (int i = 0; i < list4.Count; i++) { Peers.Remove(list4[i]); } } for (int j = 0; j < list.Count; j++) { Reject(list[j], "mandatory mod handshake timed out"); } for (int k = 0; k < list2.Count; k++) { TryInvokeRpc(list2[k], "Disconnect"); } for (int l = 0; l < list3.Count; l++) { HardDisconnect(list3[l]); lock (Sync) { Peers.Remove(list3[l]); } } } internal static void Reset() { lock (Sync) { Peers.Clear(); _serverConfirmed = false; _serverRpc = null; _nextHeartbeatTime = 0f; } } private static void SendClientHello(ZRpc rpc) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown ZPackage val = new ZPackage(); WriteClientIdentity(val); rpc.Invoke("WBI_ClientHello_V1", new object[1] { val }); WorldBoundInventoryPlugin.Log.LogDebug((object)"Mandatory-mod hello sent to server."); } private static void SendClientHeartbeat(ZRpc rpc) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown try { if (!IsRpcConnected(rpc)) { _serverConfirmed = false; return; } ZPackage val = new ZPackage(); WriteClientIdentity(val); rpc.Invoke("WBI_ClientHeartbeat_V1", new object[1] { val }); } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogDebug((object)("Could not send mandatory-mod heartbeat: " + ex.Message)); } } private static void WriteClientIdentity(ZPackage package) { package.Write(1); package.Write("fandray.valheim.worldboundinventory"); package.Write("1.1.0"); package.Write(_assemblySha256 ?? string.Empty); } private static void OnClientHello(ZRpc rpc, ZPackage package) { //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Expected O, but got Unknown if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !IsEnforcementEnabled()) { return; } try { int protocol = package.ReadInt(); string guid = package.ReadString(); string version = package.ReadString(); string text = package.ReadString(); string text2 = ValidateClient(protocol, guid, version, text); if (text2 != null) { Reject(rpc, text2); return; } lock (Sync) { if (!Peers.TryGetValue(rpc, out var value)) { value = new PeerHandshake(); Peers[rpc] = value; } value.Approved = true; value.Rejected = false; value.HeartbeatDeadline = Time.unscaledTime + GetHeartbeatGrace(); } ZPackage val = new ZPackage(); val.Write(1); val.Write("1.1.0"); val.Write(_assemblySha256 ?? string.Empty); rpc.Invoke("WBI_ServerAccepted_V1", new object[1] { val }); WorldBoundInventoryPlugin.Log.LogInfo((object)("Client passed mandatory WorldBoundInventory validation (DLL " + ShortHash(text) + ").")); } catch (Exception ex) { Reject(rpc, "malformed WorldBoundInventory handshake: " + ex.GetType().Name); } } private static void OnClientHeartbeat(ZRpc rpc, ZPackage package) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || !IsEnforcementEnabled()) { return; } try { int protocol = package.ReadInt(); string guid = package.ReadString(); string version = package.ReadString(); string assemblyHash = package.ReadString(); string text = ValidateClient(protocol, guid, version, assemblyHash); if (text != null) { Reject(rpc, "heartbeat validation failed: " + text); return; } lock (Sync) { if (!Peers.TryGetValue(rpc, out var value) || !value.Approved || value.Rejected) { Reject(rpc, "heartbeat arrived before an approved handshake"); } else { value.HeartbeatDeadline = Time.unscaledTime + GetHeartbeatGrace(); } } } catch (Exception ex) { Reject(rpc, "malformed heartbeat: " + ex.GetType().Name); } } private static void OnServerAccepted(ZRpc rpc, ZPackage package) { if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } try { int num = package.ReadInt(); string text = package.ReadString(); string hash = package.ReadString(); if (num != 1 || !string.Equals(text, "1.1.0", StringComparison.Ordinal)) { WorldBoundInventoryPlugin.Log.LogError((object)"Server sent an incompatible WorldBoundInventory confirmation."); return; } _serverConfirmed = true; _serverRpc = rpc; _nextHeartbeatTime = Time.unscaledTime + 1f; WorldBoundInventoryPlugin.Log.LogInfo((object)("Server requires and accepted WorldBoundInventory " + text + " (server DLL " + ShortHash(hash) + ").")); } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogError((object)("Invalid WorldBoundInventory server confirmation: " + ex.Message)); } } private static void OnServerDenied(ZRpc rpc, ZPackage package) { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { string text = "incompatible client mod"; try { text = package.ReadString(); } catch { } WorldBoundInventoryPlugin.Log.LogError((object)("Connection rejected by WorldBoundInventory: " + text)); } } private static string ValidateClient(int protocol, string guid, string version, string assemblyHash) { if (protocol != 1) { return "network protocol mismatch"; } if (!string.Equals(guid, "fandray.valheim.worldboundinventory", StringComparison.Ordinal)) { return "plugin identity mismatch"; } if (!string.Equals(version, "1.1.0", StringComparison.Ordinal)) { return "mod version mismatch: client " + version + ", server 1.1.0"; } if (WorldBoundInventoryPlugin.RequireExactAssembly.Value && !HashesEqual(assemblyHash, _assemblySha256)) { return "WorldBoundInventory.dll SHA-256 mismatch"; } return null; } private static void Reject(ZRpc rpc, string reason) { //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Expected O, but got Unknown if (rpc == null) { return; } bool flag = false; lock (Sync) { if (!Peers.TryGetValue(rpc, out var value)) { value = new PeerHandshake(); Peers[rpc] = value; } if (!value.Rejected) { flag = true; value.Rejected = true; value.Approved = false; value.Reason = reason; value.SoftDisconnectAt = Time.unscaledTime + 0.35f; value.HardDisconnectAt = Time.unscaledTime + 1.5f; } } if (!flag) { return; } WorldBoundInventoryPlugin.Log.LogWarning((object)("Rejected a client before world entry: " + reason + ".")); try { ZPackage val = new ZPackage(); val.Write(reason ?? "incompatible client mod"); rpc.Invoke("WBI_ServerDenied_V1", new object[1] { val }); rpc.Invoke("Error", new object[1] { 3 }); } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogDebug((object)("Could not send rejection details before disconnect: " + ex.Message)); } } private static void TryInvokeRpc(ZRpc rpc, string method) { try { if (rpc != null && IsRpcConnected(rpc)) { rpc.Invoke(method, new object[0]); } } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogDebug((object)("Soft disconnect failed: " + ex.Message)); } } private static void HardDisconnect(ZRpc rpc) { try { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && !(_getPeerByRpc == null) && !(_disconnectPeer == null)) { object? obj = _getPeerByRpc.Invoke(instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null) { _disconnectPeer.Invoke(instance, new object[1] { val }); } } } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogDebug((object)("Hard disconnect cleanup failed: " + ex.Message)); } } private static bool IsRpcConnected(ZRpc rpc) { try { return rpc != null && rpc.IsConnected(); } catch { return false; } } private static void CacheNetworkMethods() { _getPeerByRpc = AccessTools.GetDeclaredMethods(typeof(ZNet)).FirstOrDefault(delegate(MethodInfo method) { ParameterInfo[] parameters = method.GetParameters(); return method.Name == "GetPeer" && parameters.Length == 1 && typeof(ZRpc).IsAssignableFrom(parameters[0].ParameterType); }); _disconnectPeer = AccessTools.GetDeclaredMethods(typeof(ZNet)).FirstOrDefault(delegate(MethodInfo method) { ParameterInfo[] parameters = method.GetParameters(); return method.Name == "Disconnect" && parameters.Length == 1 && typeof(ZNetPeer).IsAssignableFrom(parameters[0].ParameterType); }); } private static float GetTimeout() { return Mathf.Clamp(WorldBoundInventoryPlugin.HandshakeTimeoutSeconds.Value, 3f, 30f); } private static float GetHeartbeatGrace() { return Mathf.Max(15f, GetTimeout() * 2f); } private static bool IsEnforcementEnabled() { return WorldBoundInventoryPlugin.EnforceModOnServer != null && WorldBoundInventoryPlugin.EnforceModOnServer.Value; } private static string CalculateOwnAssemblyHash() { try { string location = Assembly.GetExecutingAssembly().Location; using FileStream inputStream = new FileStream(location, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(inputStream); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } return stringBuilder.ToString(); } catch (Exception ex) { WorldBoundInventoryPlugin.Log.LogError((object)("Could not calculate WorldBoundInventory.dll SHA-256: " + ex)); return string.Empty; } } private static bool HashesEqual(string left, string right) { if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right) || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= char.ToLowerInvariant(left[i]) ^ char.ToLowerInvariant(right[i]); } return num == 0; } private static string ShortHash(string hash) { return string.IsNullOrEmpty(hash) ? "unavailable" : hash.Substring(0, Math.Min(12, hash.Length)); } }