plugins/Dyrr/Dyrr.dll

Decompiled 14 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Ezomic.Core;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("Thijssen Software")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyCopyright("Copyright (c) 2026 Robbin Thijssen")]
[assembly: AssemblyDescription("A door policy: characters that have played elsewhere do not come in.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+8af69bd1ab27754d59334b1644ea2df340513bd1")]
[assembly: AssemblyProduct("Dyrr")]
[assembly: AssemblyTitle("Dyrr")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 Dyrr
{
	internal static class Doorman
	{
		private struct Report
		{
			internal bool Readable;

			internal bool Cheats;

			internal List<long> Worlds;
		}

		private const string RpcFacts = "Dyrr_Facts";

		private const string RpcRefused = "Dyrr_Refused";

		private static readonly Dictionary<ZRpc, Report> Received = new Dictionary<ZRpc, Report>();

		[HarmonyPostfix]
		[HarmonyPatch(typeof(ZNet), "OnNewConnection")]
		private static void Handshake(ZNetPeer peer)
		{
			peer.m_rpc.Register<ZPackage>("Dyrr_Facts", (Action<ZRpc, ZPackage>)Receive);
			peer.m_rpc.Register<string>("Dyrr_Refused", (Action<ZRpc, string>)OnRefused);
			peer.m_rpc.Invoke("Dyrr_Facts", new object[1] { Facts.Gather() });
		}

		private static void Receive(ZRpc rpc, ZPackage pkg)
		{
			Report value = new Report
			{
				Worlds = new List<long>()
			};
			value.Readable = pkg.ReadBool();
			if (value.Readable)
			{
				value.Cheats = pkg.ReadBool();
				int num = pkg.ReadInt();
				for (int i = 0; i < num; i++)
				{
					value.Worlds.Add(pkg.ReadLong());
				}
			}
			Received[rpc] = value;
		}

		[HarmonyPrefix]
		[HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")]
		private static bool Judge(ZNet __instance, ZRpc rpc)
		{
			if (!DyrrConfig.Enabled.Value)
			{
				return true;
			}
			if (!__instance.IsServer())
			{
				return true;
			}
			Report value;
			bool heard = Received.TryGetValue(rpc, out value);
			string text = Verdict(__instance, heard, value);
			if (text == null)
			{
				return true;
			}
			if (!DyrrConfig.Enforce.Value)
			{
				DyrrPlugin.Log.LogWarning((object)("Would have refused a connection: " + text));
				return true;
			}
			DyrrPlugin.Log.LogWarning((object)("Refused a connection: " + text));
			rpc.Invoke("Dyrr_Refused", new object[1] { DyrrConfig.RefusedMessage.Value + " (" + text + ")" });
			rpc.Invoke("Error", new object[1] { 12 });
			return false;
		}

		private static string Verdict(ZNet net, bool heard, Report report)
		{
			if (!heard)
			{
				if (!DyrrConfig.RefuseUnreported.Value)
				{
					return null;
				}
				return "this client did not report its character";
			}
			if (!report.Readable)
			{
				if (!DyrrConfig.RefuseUnreported.Value)
				{
					return null;
				}
				return "this character's profile could not be read";
			}
			StringBuilder stringBuilder = new StringBuilder();
			if (DyrrConfig.RefuseOtherWorlds.Value)
			{
				long worldUID = net.GetWorldUID();
				int num = 0;
				foreach (long world in report.Worlds)
				{
					if (world != worldUID)
					{
						num++;
					}
				}
				if (num > 0)
				{
					stringBuilder.Append("has played on ").Append(num).Append(" other world(s)");
				}
			}
			if (DyrrConfig.RefuseCheats.Value && report.Cheats)
			{
				if (stringBuilder.Length > 0)
				{
					stringBuilder.Append(", ");
				}
				stringBuilder.Append("is flagged as having used cheats");
			}
			if (stringBuilder.Length != 0)
			{
				return stringBuilder.ToString();
			}
			return null;
		}

		private static void OnRefused(ZRpc rpc, string why)
		{
			DyrrPlugin.Log.LogError((object)("This server refused your character: " + why));
			if (DyrrPlugin.CorePresent)
			{
				ExplainOnScreen(why);
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static void ExplainOnScreen(string why)
		{
			Suite.ExplainRefusal(why);
		}

		[HarmonyPostfix]
		[HarmonyPatch(typeof(ZNet), "Disconnect")]
		private static void Forget(ZNetPeer peer)
		{
			if (peer != null && peer.m_rpc != null)
			{
				Received.Remove(peer.m_rpc);
			}
		}
	}
	internal static class DyrrConfig
	{
		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> Enforce;

		internal static ConfigEntry<bool> RefuseOtherWorlds;

		internal static ConfigEntry<bool> RefuseCheats;

		internal static ConfigEntry<bool> RefuseUnreported;

		internal static ConfigEntry<string> RefusedMessage;

		internal static ConfigEntry<bool> ProtectCharacter;

		internal static void Bind(ConfigFile cfg)
		{
			Enabled = cfg.Bind<bool>("Door", "Enabled", true, "Off leaves the plugin loaded and checking nothing. Only affects the server side; character protection has its own switch below.");
			ProtectCharacter = cfg.Bind<bool>("Protect", "ProtectCharacter", true, "Refuse to start a local world with a character that belongs to a different one, and remember which world each character belongs to.\nThis is the only half of this mod that can prevent anything. By the time the door refuses a character the damage is already permanent - the game recorded the world it visited and never removes that record. The binding lives in BepInEx/config/dyrr-home.txt, which you can edit; it only protects your own characters, so there is nothing there worth defending against you.\nIt refuses rather than asking, because a confirm dialog on an irreversible action is just a button for doing the unfixable thing.");
			Enforce = cfg.Bind<bool>("Door", "Enforce", false, "On refuses the connection. Off only logs what would have been refused.\nOff by default, and deliberately so. This is the one setting in the family that can lock people out of a server, including you, so it should be a thing somebody turns on having read what it does - not something that happens because a mod was installed.");
			RefuseOtherWorlds = cfg.Bind<bool>("Door", "RefuseOtherWorlds", true, "Refuse a character that has spawned in any world but this one.\nRead this before enabling Enforce: the game never removes entries from a character's world list, so a single visit anywhere else is permanent for that character file. Restoring a backup taken before the trip is the only way back in. That is the intended severity - it is what makes skill levels on this server mean something - but it has no undo.");
			RefuseCheats = cfg.Bind<bool>("Door", "RefuseCheats", true, "Refuse a character the game has flagged as having used cheats. Also permanent, and set by devcommands rather than by anything subtle.");
			RefuseUnreported = cfg.Bind<bool>("Door", "RefuseUnreported", true, "Refuse a connection that answers nothing, or whose profile could not be read. A door that opens when the question goes unanswered is not a door - but Core's version gate should already have turned away a client without this plugin, so in practice this is a backstop.");
			RefusedMessage = cfg.Bind<string>("Door", "RefusedMessage", "This server only accepts characters that have never played anywhere else.", "Sent to the refused client so it lands in their own log. Valheim's refusal screen carries no text of its own, and a player on somebody else's server can never read that server's log - so without this, being turned away is indistinguishable from a crash.");
		}
	}
	[BepInPlugin("ezomic.valheim.dyrr", "Dyrr", "1.0.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class DyrrPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "ezomic.valheim.dyrr";

		public const string PluginName = "Dyrr";

		public const string PluginVersion = "1.0.0";

		public const string PluginAuthor = "Robbin Thijssen";

		internal const string CoreGuid = "ezomic.valheim.core";

		private static readonly string[] LegacyGuids = new string[1] { "ezomic.valheim.threshold" };

		internal static bool CorePresent;

		internal static ManualLogSource Log;

		private Harmony _harmony;

		private static long _lastId;

		private static long _lastWorld;

		private void Awake()
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			AdoptOldConfig();
			DyrrConfig.Bind(((BaseUnityPlugin)this).Config);
			TryRegisterWithCore();
			_harmony = new Harmony("ezomic.valheim.dyrr");
			_harmony.PatchAll(typeof(Doorman));
			_harmony.PatchAll(typeof(MenuGuard));
			Log.LogInfo((object)"Dyrr 1.0.0 by Robbin Thijssen - ready.");
		}

		private void Update()
		{
			if (!DyrrConfig.ProtectCharacter.Value || (Object)(object)ZNet.instance == (Object)null || (Object)(object)Game.instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null)
			{
				return;
			}
			long worldUID = ZNet.instance.GetWorldUID();
			if (worldUID == 0L)
			{
				return;
			}
			PlayerProfile playerProfile = Game.instance.GetPlayerProfile();
			if (playerProfile != null)
			{
				long num = Home.IdOf(playerProfile);
				if (num != 0L && (num != _lastId || worldUID != _lastWorld))
				{
					_lastId = num;
					_lastWorld = worldUID;
					Home.Bind(num, playerProfile.GetName(), worldUID);
				}
			}
		}

		private void AdoptOldConfig()
		{
			string text = Path.Combine(Paths.ConfigPath, "ezomic.valheim.dyrr.cfg");
			if (File.Exists(text))
			{
				return;
			}
			string[] legacyGuids = LegacyGuids;
			foreach (string text2 in legacyGuids)
			{
				string text3 = Path.Combine(Paths.ConfigPath, text2 + ".cfg");
				if (File.Exists(text3))
				{
					try
					{
						File.Copy(text3, text);
						((BaseUnityPlugin)this).Config.Reload();
						Log.LogInfo((object)("Adopted settings from " + text3 + ". Edit " + Path.GetFileName(text) + " from now on; the old file is left alone and is no longer read."));
						break;
					}
					catch (Exception ex)
					{
						Log.LogWarning((object)("Could not adopt " + text3 + " (" + ex.Message + "), so this run uses defaults - including Enforce off. Copy it to " + Path.GetFileName(text) + " by hand to keep your settings."));
						break;
					}
				}
			}
		}

		private void TryRegisterWithCore()
		{
			CorePresent = Chainloader.PluginInfos.ContainsKey("ezomic.valheim.core");
			if (!CorePresent)
			{
				Log.LogInfo((object)"Core not installed - running standalone, without the version gate. Refused players will be told why in their log, not on the screen.");
			}
			else
			{
				RegisterWithCore();
			}
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void RegisterWithCore()
		{
			Suite.Register("ezomic.valheim.dyrr", "Dyrr", "1.0.0", ((BaseUnityPlugin)this).Config, (Requirement)0, (Assembly)null);
			Suite.Sync((ConfigEntryBase[])(object)new ConfigEntryBase[5]
			{
				(ConfigEntryBase)DyrrConfig.Enabled,
				(ConfigEntryBase)DyrrConfig.Enforce,
				(ConfigEntryBase)DyrrConfig.RefuseOtherWorlds,
				(ConfigEntryBase)DyrrConfig.RefuseCheats,
				(ConfigEntryBase)DyrrConfig.RefuseUnreported
			});
		}

		private void OnDestroy()
		{
			if (_harmony != null)
			{
				_harmony.UnpatchSelf();
			}
		}
	}
	internal static class Facts
	{
		private sealed class FieldInfoCache
		{
			internal FieldInfo Field;
		}

		private static FieldInfoCache _worldData;

		internal static ZPackage Gather()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			List<long> list = new List<long>();
			bool flag = false;
			try
			{
				PlayerProfile val2 = (((Object)(object)Game.instance != (Object)null) ? Game.instance.GetPlayerProfile() : null);
				if (val2 != null)
				{
					flag = val2.m_usedCheats;
					CollectWorlds(val2, list);
				}
			}
			catch (Exception ex)
			{
				DyrrPlugin.Log.LogWarning((object)("Could not read this character's profile: " + ex.Message));
				val.Write(false);
				return val;
			}
			val.Write(true);
			val.Write(flag);
			val.Write(list.Count);
			foreach (long item in list)
			{
				val.Write(item);
			}
			return val;
		}

		private static void CollectWorlds(PlayerProfile profile, List<long> into)
		{
			if (_worldData == null)
			{
				_worldData = new FieldInfoCache
				{
					Field = AccessTools.Field(typeof(PlayerProfile), "m_worldData")
				};
			}
			if (_worldData.Field == null)
			{
				DyrrPlugin.Log.LogError((object)"PlayerProfile.m_worldData not found - this character's travel cannot be seen.");
			}
			else
			{
				if (!(_worldData.Field.GetValue(profile) is IDictionary dictionary))
				{
					return;
				}
				foreach (DictionaryEntry item2 in dictionary)
				{
					if (item2.Key is long item)
					{
						into.Add(item);
					}
				}
			}
		}
	}
	internal static class Home
	{
		private static readonly Dictionary<long, long> _homes = new Dictionary<long, long>();

		private static FieldInfo _playerId;

		private static bool _loaded;

		private static readonly string[] LegacyNames = new string[2] { "threshold-home.txt", "boon-home.txt" };

		private static string HomePath => Path.Combine(Paths.ConfigPath, "dyrr-home.txt");

		internal static long IdOf(PlayerProfile profile)
		{
			if (profile == null)
			{
				return 0L;
			}
			if (_playerId == null)
			{
				_playerId = AccessTools.Field(typeof(PlayerProfile), "m_playerID");
			}
			if (_playerId == null)
			{
				DyrrPlugin.Log.LogError((object)"PlayerProfile.m_playerID not found - character protection is off.");
				return 0L;
			}
			object value = _playerId.GetValue(profile);
			if (value is long)
			{
				return (long)value;
			}
			return 0L;
		}

		internal static long Get(long playerId)
		{
			Load();
			if (playerId == 0L)
			{
				return 0L;
			}
			if (!_homes.TryGetValue(playerId, out var value))
			{
				return 0L;
			}
			return value;
		}

		internal static void Bind(long playerId, string name, long worldUid)
		{
			Load();
			if (playerId == 0L || worldUid == 0L)
			{
				return;
			}
			if (_homes.TryGetValue(playerId, out var value))
			{
				if (value != worldUid)
				{
					DyrrPlugin.Log.LogWarning((object)("Character '" + name + "' (" + playerId + ") is bound to world " + value + " but is in world " + worldUid + ". Too late to stop it - that world is now written into the character."));
				}
			}
			else
			{
				_homes[playerId] = worldUid;
				Save();
				DyrrPlugin.Log.LogInfo((object)("Bound character '" + name + "' (" + playerId + ") to world " + worldUid + "."));
			}
		}

		internal static void Forget(long playerId)
		{
			Load();
			if (_homes.Remove(playerId))
			{
				Save();
			}
		}

		private static void Load()
		{
			if (_loaded)
			{
				return;
			}
			_loaded = true;
			string text = (File.Exists(HomePath) ? HomePath : null);
			string[] legacyNames;
			if (text == null)
			{
				legacyNames = LegacyNames;
				foreach (string path in legacyNames)
				{
					string text2 = Path.Combine(Paths.ConfigPath, path);
					if (File.Exists(text2))
					{
						text = text2;
						break;
					}
				}
			}
			if (text == null)
			{
				return;
			}
			bool flag = text != HomePath;
			if (flag)
			{
				DyrrPlugin.Log.LogInfo((object)("Adopting character bindings from " + text + "; they will be written to " + HomePath + " from now on."));
			}
			legacyNames = File.ReadAllLines(text);
			for (int i = 0; i < legacyNames.Length; i++)
			{
				string text3 = legacyNames[i].Trim();
				if (text3.Length != 0 && text3[0] != '#')
				{
					string[] array = text3.Split(new char[1] { '|' });
					if (array.Length == 2 && long.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && long.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						_homes[result] = result2;
					}
				}
			}
			if (flag)
			{
				Save();
			}
		}

		private static void Save()
		{
			try
			{
				List<string> list = new List<string> { "# Which world each character belongs to: playerId|worldUid", "# Dyrr refuses to start a character in any other world, because doing", "# so permanently records that world in the character and locks it out of", "# its own server. Delete a line to unbind that character." };
				foreach (KeyValuePair<long, long> home in _homes)
				{
					list.Add(home.Key + "|" + home.Value);
				}
				File.WriteAllLines(HomePath, list.ToArray());
			}
			catch (Exception ex)
			{
				DyrrPlugin.Log.LogError((object)("Could not write " + HomePath + ": " + ex.Message));
			}
		}
	}
	internal static class MenuGuard
	{
		[CompilerGenerated]
		private static class <>O
		{
			public static PopupButtonCallback <0>__Pop;
		}

		private static FieldInfo _world;

		private static FieldInfo _profiles;

		private static FieldInfo _profileIndex;

		[HarmonyPrefix]
		[HarmonyPatch(typeof(FejdStartup), "OnWorldStart")]
		private static bool GuardWorldStart(FejdStartup __instance)
		{
			//IL_0166: Unknown result type (might be due to invalid IL or missing references)
			//IL_0170: Expected O, but got Unknown
			//IL_015a: Unknown result type (might be due to invalid IL or missing references)
			//IL_015f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0165: Expected O, but got Unknown
			if (!DyrrConfig.ProtectCharacter.Value)
			{
				return true;
			}
			World val = SelectedWorld(__instance);
			PlayerProfile val2 = SelectedProfile(__instance);
			if (val == null || val2 == null)
			{
				return true;
			}
			long num = Home.IdOf(val2);
			long num2 = Home.Get(num);
			if (num == 0L || num2 == 0L || num2 == val.m_uid)
			{
				return true;
			}
			DyrrPlugin.Log.LogWarning((object)("Character '" + val2.GetName() + "' (" + num + ") belongs to world " + num2 + " but is being started in world '" + val.m_name + "' (" + val.m_uid + "). Refused."));
			string text = "Character: " + val2.GetName() + "  (id " + num + ")\nThis world: " + val.m_name + "  (" + val.m_uid + ")\nBelongs to world: " + num2 + "\n\nLoading it here records this world permanently in the character and locks it out of its own server. The only undo is restoring a backup, so this is refused rather than confirmed.\n\nIf the binding is wrong, delete the line starting " + num + " from BepInEx/config/dyrr-home.txt.";
			object obj = <>O.<0>__Pop;
			if (obj == null)
			{
				PopupButtonCallback val3 = UnifiedPopup.Pop;
				<>O.<0>__Pop = val3;
				obj = (object)val3;
			}
			UnifiedPopup.Push((PopupBase)new WarningPopup("Dyrr: wrong world for this character", text, (PopupButtonCallback)obj, false));
			return false;
		}

		private static World SelectedWorld(FejdStartup fejd)
		{
			if (_world == null)
			{
				_world = AccessTools.Field(typeof(FejdStartup), "m_world");
			}
			if (!(_world != null))
			{
				return null;
			}
			object? value = _world.GetValue(fejd);
			return (World)((value is World) ? value : null);
		}

		private static PlayerProfile SelectedProfile(FejdStartup fejd)
		{
			if (_profiles == null)
			{
				_profiles = AccessTools.Field(typeof(FejdStartup), "m_profiles");
			}
			if (_profileIndex == null)
			{
				_profileIndex = AccessTools.Field(typeof(FejdStartup), "m_profileIndex");
			}
			if (_profiles == null || _profileIndex == null)
			{
				return null;
			}
			if (!(_profiles.GetValue(fejd) is List<PlayerProfile> list))
			{
				return null;
			}
			int num = (int)_profileIndex.GetValue(fejd);
			if (num < 0 || num >= list.Count)
			{
				return null;
			}
			return list[num];
		}
	}
}