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 Policy { get; private set; } internal static ConfigEntry 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("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("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 LoggedKeys = new HashSet(StringComparer.Ordinal); internal static IEnumerable Transpiler(IEnumerable 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(Dictionary 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 key, DuplicatePolicy policy, bool merged, string details) { if (Plugin.LogDuplicates != null && !Plugin.LogDuplicates.Value) { return; } string text = ((key == 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 AccessorCache = new Dictionary(); internal static bool TryMerge(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("k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic), type.GetField("k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic), type.GetField("k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); AccessorCache[type] = value; return value; } } } }