Decompiled source of ServersideQoL DuplicateSummonFix v0.1.2

plugins\ServersideQoL.DuplicateSummonFix.dll

Decompiled 20 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("ServersideQoL Duplicate Summon Fix")]
[assembly: AssemblyDescription("Unofficial server-only compatibility fix for duplicate summon animation keys in ServersideQoL 1.8.0.")]
[assembly: AssemblyProduct("ServersideQoL Duplicate Summon Fix")]
[assembly: AssemblyCopyright("Copyright (c) 2026 laredson")]
[assembly: AssemblyFileVersion("0.1.2.0")]
[assembly: AssemblyInformationalVersion("0.1.2")]
[assembly: AssemblyMetadata("Author", "laredson")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("0.1.2.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 Laredson.ServersideQoLDuplicateSummonFix
{
	[BepInPlugin("laredson.serversideqol.duplicatesummonfix", "ServersideQoL Duplicate Summon Fix", "0.1.2")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "laredson.serversideqol.duplicatesummonfix";

		public const string PluginName = "ServersideQoL Duplicate Summon Fix";

		public const string PluginVersion = "0.1.2";

		public const string ServersideQoLGuid = "argusmagnus.ServersideQoL";

		private Harmony _harmony;

		internal static ManualLogSource Log { get; private set; }

		internal static ConfigEntry<DuplicatePolicy> Policy { get; private set; }

		internal static ConfigEntry<bool> LogDuplicates { get; private set; }

		private void Awake()
		{
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ac: Expected O, but got Unknown
			//IL_00c5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d1: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Policy = ((BaseUnityPlugin)this).Config.Bind<DuplicatePolicy>("General", "DuplicatePolicy", DuplicatePolicy.Merge, "How duplicate summon animation keys are handled. Merge is recommended. KeepFirst and KeepLast are diagnostic fallback modes.");
			LogDuplicates = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "LogDuplicateKeys", true, "Log each duplicate animation key once.");
			Type type = AccessTools.TypeByName("Valheim.ServersideQoL.Processors.PlayerSpawnedProcessor");
			MethodInfo methodInfo = ((type == null) ? null : AccessTools.Method(type, "Initialize", new Type[1] { typeof(bool) }, (Type[])null));
			if (methodInfo == null)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Could not find PlayerSpawnedProcessor.Initialize(bool). The installed ServersideQoL version is not compatible with this patch.");
				return;
			}
			_harmony = new Harmony("laredson.serversideqol.duplicatesummonfix");
			_harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(PlayerSpawnedProcessorPatch), "Transpiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Patch loaded. Duplicate summon animation keys will no longer abort initialization.");
		}

		private void OnDestroy()
		{
			if (_harmony != null)
			{
				_harmony.UnpatchSelf();
			}
		}
	}
	public enum DuplicatePolicy
	{
		Merge,
		KeepFirst,
		KeepLast
	}
	internal static class PlayerSpawnedProcessorPatch
	{
		private static readonly MethodInfo AddOrResolveDefinition = AccessTools.Method(typeof(PlayerSpawnedProcessorPatch), "AddOrResolve", (Type[])null, (Type[])null);

		private static readonly object LoggedKeysLock = new object();

		private static readonly HashSet<string> LoggedKeys = new HashSet<string>(StringComparer.Ordinal);

		internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			int replacements = 0;
			foreach (CodeInstruction instruction in instructions)
			{
				if (IsTargetDictionaryAdd(instruction, out var keyType, out var valueType))
				{
					MethodInfo operand = AddOrResolveDefinition.MakeGenericMethod(keyType, valueType);
					instruction.opcode = OpCodes.Call;
					instruction.operand = operand;
					replacements++;
					yield return instruction;
				}
				else
				{
					yield return instruction;
				}
			}
			switch (replacements)
			{
			case 1:
				Plugin.Log.LogInfo((object)"Patched PlayerSpawnedProcessor summon animation dictionary insertion.");
				break;
			case 0:
				Plugin.Log.LogError((object)"Expected dictionary insertion was not found. No compatibility fix was applied.");
				break;
			default:
				Plugin.Log.LogWarning((object)("Patched " + replacements + " matching dictionary insertions; expected exactly one."));
				break;
			}
		}

		private static bool IsTargetDictionaryAdd(CodeInstruction instruction, out Type keyType, out Type valueType)
		{
			keyType = null;
			valueType = null;
			if (instruction.opcode != OpCodes.Callvirt)
			{
				return false;
			}
			MethodInfo methodInfo = instruction.operand as MethodInfo;
			if (methodInfo == null || methodInfo.Name != "Add" || methodInfo.DeclaringType == null)
			{
				return false;
			}
			Type declaringType = methodInfo.DeclaringType;
			if (!declaringType.IsGenericType || declaringType.GetGenericTypeDefinition() != typeof(Dictionary<, >))
			{
				return false;
			}
			Type[] genericArguments = declaringType.GetGenericArguments();
			if (genericArguments.Length != 2 || genericArguments[0] != typeof(string))
			{
				return false;
			}
			Type type = genericArguments[1];
			Type declaringType2 = type.DeclaringType;
			if (type.Name != "SpawnInfo" || declaringType2 == null || declaringType2.Name != "PlayerSpawnedProcessor")
			{
				return false;
			}
			keyType = genericArguments[0];
			valueType = type;
			return true;
		}

		private static void AddOrResolve<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key, TValue incoming)
		{
			if (!dictionary.TryGetValue(key, out var value))
			{
				dictionary.Add(key, incoming);
				return;
			}
			DuplicatePolicy duplicatePolicy = ((Plugin.Policy != null) ? Plugin.Policy.Value : DuplicatePolicy.Merge);
			bool flag = false;
			string details = string.Empty;
			switch (duplicatePolicy)
			{
			case DuplicatePolicy.KeepLast:
				dictionary[key] = incoming;
				details = "kept the last mapping";
				break;
			case DuplicatePolicy.KeepFirst:
				details = "kept the first mapping";
				break;
			default:
				flag = SpawnInfoMerger.TryMerge(value, incoming, out details);
				if (!flag)
				{
					details = "merge failed; kept the first mapping. " + details;
				}
				break;
			}
			LogDuplicateOnce(key, duplicatePolicy, flag, details);
		}

		private static void LogDuplicateOnce<TKey>(TKey key, DuplicatePolicy policy, bool merged, string details)
		{
			if (Plugin.LogDuplicates != null && !Plugin.LogDuplicates.Value)
			{
				return;
			}
			string text = ((key == null) ? "<null>" : key.ToString());
			lock (LoggedKeysLock)
			{
				if (!LoggedKeys.Add(text))
				{
					return;
				}
			}
			Plugin.Log.LogWarning((object)("Duplicate summon animation key '" + text + "' detected. Policy=" + policy.ToString() + "; merged=" + merged + ". " + details));
		}
	}
	internal static class SpawnInfoMerger
	{
		private sealed class Accessors
		{
			internal FieldInfo MaxSpawned { get; private set; }

			internal FieldInfo MaxSummonReached { get; private set; }

			internal FieldInfo SpawnedByPrefab { get; private set; }

			internal bool IsValid
			{
				get
				{
					if (MaxSpawned != null && MaxSummonReached != null)
					{
						return SpawnedByPrefab != null;
					}
					return false;
				}
			}

			internal Accessors(FieldInfo maxSpawned, FieldInfo maxSummonReached, FieldInfo spawnedByPrefab)
			{
				MaxSpawned = maxSpawned;
				MaxSummonReached = maxSummonReached;
				SpawnedByPrefab = spawnedByPrefab;
			}
		}

		private static readonly object AccessorLock = new object();

		private static readonly Dictionary<Type, Accessors> AccessorCache = new Dictionary<Type, Accessors>();

		internal static bool TryMerge<TValue>(TValue existing, TValue incoming, out string details)
		{
			details = string.Empty;
			object obj = existing;
			object obj2 = incoming;
			if (obj == null || obj2 == null)
			{
				details = "one SpawnInfo value was null";
				return false;
			}
			Type type = obj.GetType();
			if (obj2.GetType() != type)
			{
				details = "SpawnInfo values had different runtime types";
				return false;
			}
			try
			{
				Accessors accessors = GetAccessors(type);
				if (!accessors.IsValid)
				{
					details = "required SpawnInfo backing fields were not found";
					return false;
				}
				int val = (int)accessors.MaxSpawned.GetValue(obj);
				int val2 = (int)accessors.MaxSpawned.GetValue(obj2);
				int num = Math.Max(val, val2);
				accessors.MaxSpawned.SetValue(obj, num);
				string value = accessors.MaxSummonReached.GetValue(obj) as string;
				string value2 = accessors.MaxSummonReached.GetValue(obj2) as string;
				if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(value2))
				{
					accessors.MaxSummonReached.SetValue(obj, value2);
				}
				IDictionary dictionary = accessors.SpawnedByPrefab.GetValue(obj) as IDictionary;
				IDictionary dictionary2 = accessors.SpawnedByPrefab.GetValue(obj2) as IDictionary;
				if (dictionary == null || dictionary2 == null)
				{
					details = "SpawnedByPrefab was not an IDictionary";
					return false;
				}
				int num2 = 0;
				int num3 = 0;
				foreach (DictionaryEntry item in dictionary2)
				{
					if (!dictionary.Contains(item.Key))
					{
						dictionary.Add(item.Key, item.Value);
						num2++;
						continue;
					}
					IList list = dictionary[item.Key] as IList;
					IEnumerable enumerable = item.Value as IEnumerable;
					if (list == null || enumerable == null)
					{
						continue;
					}
					foreach (object item2 in enumerable)
					{
						if (!list.Contains(item2))
						{
							list.Add(item2);
							num3++;
						}
					}
				}
				details = "merged SpawnInfo; MaxSpawned=" + num + ", added prefab mappings=" + num2 + ", added tracked summon IDs=" + num3 + ". Duplicate staves sharing one animation also share this combined cap.";
				return true;
			}
			catch (Exception ex)
			{
				details = ex.GetType().Name + ": " + ex.Message;
				Plugin.Log.LogError((object)("Failed to merge duplicate SpawnInfo values: " + ex));
				return false;
			}
		}

		private static Accessors GetAccessors(Type type)
		{
			lock (AccessorLock)
			{
				if (AccessorCache.TryGetValue(type, out var value))
				{
					return value;
				}
				value = new Accessors(type.GetField("<MaxSpawned>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic), type.GetField("<MaxSummonReached>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic), type.GetField("<SpawnedByPrefab>k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
				AccessorCache[type] = value;
				return value;
			}
		}
	}
}