Decompiled source of WorldBoundInventory v1.1.0

IndividualInventory.dll

Decompiled 3 hours ago
using 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));
	}
}