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 SummonChoise v1.0.0
BepInEx/plugins/SkeletonChoice/SkeletonChoice.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; 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; 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("SkeletonChoice")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+e84df9328a7b5b18d693b0690420394635269727")] [assembly: AssemblyProduct("SkeletonChoice")] [assembly: AssemblyTitle("SkeletonChoice")] [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 SkeletonChoice { public enum SummonMode { Random, Warrior, Archer, ShieldWarrior } internal enum EquipmentKind { Other, Bow, Sword, Shield, OtherWeapon } internal sealed class EquipmentItem { internal readonly EquipmentKind Kind; internal readonly int Slot; internal EquipmentItem(EquipmentKind kind, int slot = 0) { Kind = kind; Slot = slot; } } internal sealed class ChanceItem { internal EquipmentItem Item; internal float Chance; } internal sealed class EquipmentTable { internal EquipmentItem[] Defaults = new EquipmentItem[0]; internal EquipmentItem[] Shields = new EquipmentItem[0]; internal EquipmentItem[] Weapons = new EquipmentItem[0]; internal EquipmentItem[] Armor = new EquipmentItem[0]; internal EquipmentItem[][] Sets = new EquipmentItem[0][]; internal ChanceItem[] Items = new ChanceItem[0]; } internal interface ISeedRandom { void Initialize(int seed); int Index(int count); float Value(); } internal struct EquipmentResult { internal int Bows; internal int Swords; internal int Shields; internal int OtherWeapons; internal void Add(EquipmentItem item) { if (item != null) { switch (item.Kind) { case EquipmentKind.Bow: Bows++; break; case EquipmentKind.Sword: Swords++; break; case EquipmentKind.Shield: Shields++; break; case EquipmentKind.OtherWeapon: OtherWeapons++; break; } } } internal bool Matches(SummonMode mode) { switch (mode) { case SummonMode.Random: return true; case SummonMode.Archer: if (Bows > 0 && Swords == 0 && OtherWeapons == 0) { return Shields == 0; } return false; case SummonMode.Warrior: if (Swords > 0 && Bows == 0 && OtherWeapons == 0) { return Shields == 0; } return false; case SummonMode.ShieldWarrior: if (Swords > 0 && Bows == 0 && OtherWeapons == 0) { return Shields > 0; } return false; default: return false; } } } internal static class SelectionPolicy { internal static EquipmentResult Evaluate(EquipmentTable table, ISeedRandom random) { EquipmentResult result = default(EquipmentResult); EquipmentItem[] defaults = table.Defaults; foreach (EquipmentItem item in defaults) { result.Add(item); } AddRandom(table.Shields, random, ref result); AddRandom(table.Weapons, random, ref result); AddRandom(table.Armor, random, ref result); if (table.Sets.Length != 0) { defaults = table.Sets[random.Index(table.Sets.Length)]; foreach (EquipmentItem item2 in defaults) { result.Add(item2); } } HashSet<int> hashSet = new HashSet<int>(); ChanceItem[] items = table.Items; foreach (ChanceItem chanceItem in items) { if (chanceItem.Item != null && random.Value() > chanceItem.Chance && hashSet.Add(chanceItem.Item.Slot)) { result.Add(chanceItem.Item); } } return result; } private static void AddRandom(EquipmentItem[] items, ISeedRandom random, ref EquipmentResult result) { if (items.Length != 0) { result.Add(items[random.Index(items.Length)]); } } internal static bool TryChoose(EquipmentTable table, SummonMode mode, int originalSeed, ISeedRandom random, out int selectedSeed, int attempts = 4096) { selectedSeed = originalSeed; if (mode == SummonMode.Random) { return true; } for (int i = 0; i < attempts; i++) { int num = originalSeed + i * -1640531527; if (num != 0) { random.Initialize(num); if (Evaluate(table, random).Matches(mode)) { selectedSeed = num; return true; } } } return false; } } public enum SpiritMode { Missing, Random, Bear, Moose, Wolf, Boar } internal static class SpiritSelectionPolicy { internal const int Vanilla = -2; internal const int Unavailable = -1; internal static int Choose(SpiritMode mode, IReadOnlyList<SpiritMode> candidates, IReadOnlyDictionary<SpiritMode, int> occupied, int capacity) { if (mode == SpiritMode.Random) { return -2; } int num = -1; int num2 = capacity; for (int i = 0; i < candidates.Count; i++) { SpiritMode spiritMode = candidates[i]; if (spiritMode < SpiritMode.Bear || spiritMode > SpiritMode.Boar) { continue; } if (mode != SpiritMode.Missing) { if (spiritMode == mode) { return i; } continue; } int value; int num3 = (occupied.TryGetValue(spiritMode, out value) ? value : 0); if (num3 < num2 || (num >= 0 && num3 == num2 && spiritMode < candidates[num])) { num = i; num2 = num3; } } return num; } internal static void Add(IDictionary<SpiritMode, int> occupied, SpiritMode species) { occupied[species] = ((!occupied.TryGetValue(species, out var value)) ? 1 : (value + 1)); } } internal sealed class SummonSnapshot { internal readonly string Id; internal readonly long Summoner; internal readonly bool Exists; internal readonly float Health; internal readonly bool Loaded; internal readonly bool Unsummoning; internal readonly bool HasFollowTarget; internal readonly float FollowDistance; internal readonly float UnsummonDistance; internal SummonSnapshot(string id, long summoner, bool exists, float health, bool loaded = true, bool unsummoning = false, bool hasFollowTarget = false, float followDistance = 0f, float unsummonDistance = 0f) { Id = id; Summoner = summoner; Exists = exists; Health = health; Loaded = loaded; Unsummoning = unsummoning; HasFollowTarget = hasFollowTarget; FollowDistance = followDistance; UnsummonDistance = unsummonDistance; } } internal static class SummonCountPolicy { internal static int Count(long playerId, IEnumerable<SummonSnapshot> summons) { if (playerId == 0L) { return 0; } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); int num = 0; foreach (SummonSnapshot summon in summons) { if (IsActive(playerId, summon) && hashSet.Add(summon.Id)) { num++; } } return num; } internal static bool IsActive(long playerId, SummonSnapshot summon) { if (playerId != 0L && summon != null && summon.Exists && summon.Loaded && !summon.Unsummoning && summon.Summoner == playerId && summon.Health > 0f && !string.IsNullOrEmpty(summon.Id)) { if (summon.HasFollowTarget && summon.UnsummonDistance > 0f) { return !(summon.FollowDistance > summon.UnsummonDistance); } return true; } return false; } internal static bool Visible(bool enabled, bool showBuff, bool localPlayerAlive, int count) { if (enabled && showBuff && localPlayerAlive) { return count > 0; } return false; } } [BepInPlugin("Slikfoul.SkeletonChoice", "Summon Choise", "1.0.0")] [BepInIncompatibility("local.valheim.skeletonselector")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "Slikfoul.SkeletonChoice"; public const string PluginName = "Summon Choise"; public const string PluginVersion = "1.0.0"; internal static Plugin Instance; private Harmony harmony; private ConfigEntry<bool> modEnabled; private ConfigEntry<SummonMode> mode; private ConfigEntry<bool> debug; private ConfigEntry<bool> showSummonBuff; private ConfigEntry<SpiritMode> spirit; private ConfigEntry<bool> showSpiritBuff; private ConfigEntry<int> spiritFollowDistance; internal ManualLogSource Log => ((BaseUnityPlugin)this).Logger; internal bool ModEnabled => modEnabled.Value; internal SummonMode SelectedMode { get { if (!Enum.IsDefined(typeof(SummonMode), mode.Value)) { return SummonMode.Random; } return mode.Value; } } internal bool DebugLogging => debug.Value; internal bool ShowSummonBuff => showSummonBuff.Value; internal SpiritMode SelectedSpirit { get { if (!Enum.IsDefined(typeof(SpiritMode), spirit.Value)) { return SpiritMode.Missing; } return spirit.Value; } } internal bool ShowSpiritBuff => showSpiritBuff.Value; internal int SpiritFollowDistanceMeters => spiritFollowDistance.Value; private void Awake() { //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Expected O, but got Unknown Instance = this; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; modEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - General", "Enabled", true, "Enable summon selection, spirit follow distance, and status indicators."); mode = ((BaseUnityPlugin)this).Config.Bind<SummonMode>("1 - General", "Skeleton type", SummonMode.Random, "Random: vanilla selection. Warrior: sword without a shield. Archer: bow without a shield. ShieldWarrior: sword and shield. Applies to future summons and persists across restarts."); showSummonBuff = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Display", "Show summon buff", true, "Show a status icon with your active loaded skeleton count. Excludes skeletons that are dead, unloading, unsummoning, or beyond their vanilla follow-distance limit."); spirit = ((BaseUnityPlugin)this).Config.Bind<SpiritMode>("1 - General", "Spirit Caller type", SpiritMode.Missing, "Missing: fill spirit slots in rounds (Bear, Moose, Wolf, Boar), up to the staff's limit per species. Random: vanilla selection. Select a species to summon only that spirit."); spiritFollowDistance = ((BaseUnityPlugin)this).Config.Bind<int>("1 - General", "Spirit follow distance", 6, new ConfigDescription("Distance in meters at which your Spirit Caller spirits stop following you outside combat. 3 uses the game's usual distance.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(3, 10), Array.Empty<object>())); showSpiritBuff = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Display", "Show spirit buff", true, "Show a separate status icon with your active Spirit Caller summon count."); debug = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Diagnostics", "Log summons", false, "Log equipment selection for every summon."); RemoveLegacySetting("2 - Interface", "Menu key"); RemoveLegacySetting("2 - Interface", "Show hint"); ((BaseUnityPlugin)this).Config.SaveOnConfigSet = true; ((BaseUnityPlugin)this).Config.Save(); try { SummonHooks.VerifyHooks(); harmony = new Harmony("Slikfoul.SkeletonChoice"); harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Summon Choise 1.0.0 loaded. Dead Raiser and Spirit Caller hooks verified."); } catch (Exception ex) { if (harmony != null) { harmony.UnpatchSelf(); } modEnabled.Value = false; ((BaseUnityPlugin)this).Logger.LogError((object)("Summon Choise disabled: summoning hooks are incompatible. " + ex)); } } private void RemoveLegacySetting(string section, string key) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown ConfigDefinition val = new ConfigDefinition(section, key); ((BaseUnityPlugin)this).Config.Bind<string>(val, "", new ConfigDescription("", (AcceptableValueBase)null, Array.Empty<object>())); ((BaseUnityPlugin)this).Config.Remove(val); } private void Update() { SummonIndicator.Update(); } private void OnDestroy() { SummonIndicator.Clear(); if (harmony != null) { harmony.UnpatchSelf(); } SummonHooks.Clear(); if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } internal sealed class SpiritCast { internal GameObject[] Prefabs; internal SpiritMode Species; internal SpawnAbility Ability; internal Player Caster; internal float Expires; } internal static class SpiritCaller { [HarmonyPatch(typeof(Humanoid), "StartAttack")] private static class GuardFullRoster { private static bool Prefix(Humanoid __instance, bool secondaryAttack, ref bool __result) { Plugin instance = Plugin.Instance; Player localPlayer = Player.m_localPlayer; if (secondaryAttack || (Object)(object)instance == (Object)null || !instance.ModEnabled || (Object)(object)localPlayer == (Object)null || (Object)(object)__instance != (Object)(object)localPlayer) { return true; } SpiritMode selectedSpirit = instance.SelectedSpirit; if (selectedSpirit == SpiritMode.Random) { return true; } ItemData currentWeapon = ((Humanoid)localPlayer).GetCurrentWeapon(); if (!IsStaff(currentWeapon)) { return true; } GameObject attackProjectile = currentWeapon.m_shared.m_attack.m_attackProjectile; SpawnAbility val = (((Object)(object)attackProjectile != (Object)null) ? attackProjectile.GetComponent<SpawnAbility>() : null); if ((Object)(object)val == (Object)null) { return true; } SpiritMode[] array = Candidates(val.m_spawnPrefab); if (!Array.Exists(array, (SpiritMode x) => x >= SpiritMode.Bear && x <= SpiritMode.Boar)) { return true; } if (SpiritSelectionPolicy.Choose(selectedSpirit, array, (selectedSpirit == SpiritMode.Missing) ? Occupied(localPlayer) : null, Capacity(val, currentWeapon)) != -1) { return true; } Notify(localPlayer, selectedSpirit); __result = false; return false; } } internal const string Staff = "StaffSpiritCaller"; private static readonly Dictionary<int, SpiritMode> types = new Dictionary<int, SpiritMode> { { StringExtensionMethods.GetStableHashCode("Bjorn_spiritcaller"), SpiritMode.Bear }, { StringExtensionMethods.GetStableHashCode("Moose_spiritcaller"), SpiritMode.Moose }, { StringExtensionMethods.GetStableHashCode("Wolf_spiritcaller"), SpiritMode.Wolf }, { StringExtensionMethods.GetStableHashCode("Boar_spiritcaller"), SpiritMode.Boar } }; private static readonly List<SpiritCast> pending = new List<SpiritCast>(); internal static bool IsStaff(ItemData item) { if (item != null && (Object)(object)item.m_dropPrefab != (Object)null) { return ((Object)item.m_dropPrefab).name == "StaffSpiritCaller"; } return false; } internal static bool TryGetSpecies(int prefabHash, out SpiritMode species) { return types.TryGetValue(prefabHash, out species); } private static SpiritMode[] Candidates(GameObject[] prefabs) { if (prefabs == null) { return new SpiritMode[0]; } SpiritMode[] array = new SpiritMode[prefabs.Length]; for (int i = 0; i < prefabs.Length; i++) { if ((Object)(object)prefabs[i] != (Object)null) { TryGetSpecies(StringExtensionMethods.GetStableHashCode(((Object)prefabs[i]).name), out array[i]); } } return array; } private static Dictionary<SpiritMode, int> Occupied(Player caster) { Dictionary<SpiritMode, int> dictionary = new Dictionary<SpiritMode, int>(); SummonIndicator.CollectSpirits(caster, dictionary); for (int num = pending.Count - 1; num >= 0; num--) { SpiritCast spiritCast = pending[num]; if ((Object)(object)spiritCast.Ability == (Object)null || (Object)(object)spiritCast.Caster != (Object)(object)caster || Time.time > spiritCast.Expires) { pending.RemoveAt(num); } else { SpiritSelectionPolicy.Add(dictionary, spiritCast.Species); } } return dictionary; } private static int Capacity(SpawnAbility ability, ItemData weapon) { if (!ability.m_setMaxInstancesFromWeaponLevel) { return 1; } return Mathf.Max(1, weapon.m_quality); } internal static bool Prepare(SpawnAbility ability, Player caster, ItemData weapon, SpiritMode mode, out SpiritCast cast) { //IL_00b7: Unknown result type (might be due to invalid IL or missing references) cast = null; SpiritMode[] array = Candidates(ability.m_spawnPrefab); if (!Array.Exists(array, (SpiritMode x) => x >= SpiritMode.Bear && x <= SpiritMode.Boar)) { return true; } Dictionary<SpiritMode, int> dictionary = ((mode == SpiritMode.Missing) ? Occupied(caster) : null); int capacity = Capacity(ability, weapon); int num = SpiritSelectionPolicy.Choose(mode, array, dictionary, capacity); if (num == -1) { Notify(caster, mode); return false; } cast = new SpiritCast { Ability = ability, Caster = caster }; if (num == -2) { return true; } if (dictionary != null && ability.m_maxSpawned > 0) { SpiritMode[] array2 = (SpiritMode[])array.Clone(); for (int num2 = 0; num2 < array2.Length; num2++) { if (array2[num2] >= SpiritMode.Bear && SpawnSystem.GetNrOfInstances(ability.m_spawnPrefab[num2], ((Component)caster).transform.position, 0f, false, false) >= ability.m_maxSpawned) { array2[num2] = SpiritMode.Missing; } } int num3 = SpiritSelectionPolicy.Choose(mode, array2, dictionary, capacity); if (num3 >= 0) { num = num3; } } cast.Prefabs = (GameObject[])(object)new GameObject[1] { ability.m_spawnPrefab[num] }; cast.Species = array[num]; cast.Expires = Time.time + Mathf.Max(0f, ability.m_initialSpawnDelay) + Mathf.Max(0f, ability.m_preSpawnDelay) + 15f; pending.Add(cast); return true; } private static void Notify(Player caster, SpiritMode mode) { ((Character)caster).Message((MessageType)2, (mode == SpiritMode.Missing) ? "All spirit slots for this staff are filled or reserved." : "The selected spirit is not available for this staff.", 0, (Sprite)null, false); } internal static void Release(SpiritCast cast) { if (cast != null) { pending.Remove(cast); } } internal static void Clear() { pending.Clear(); } } [HarmonyPatch(typeof(BaseAI), "Follow")] internal static class SpiritFollowDistance { private const float VanillaDistance = 3f; private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Expected O, but got Unknown //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Expected O, but got Unknown //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Expected O, but got Unknown List<CodeInstruction> list = new List<CodeInstruction>(instructions); int num = list.FindIndex(IsStopDistance); if (num < 0 || num != list.FindLastIndex(IsStopDistance)) { Plugin.Instance.Log.LogWarning((object)"Spirit follow distance unavailable: the game's follow method has changed."); return list; } CodeInstruction val = list[num]; CodeInstruction val2 = new CodeInstruction(OpCodes.Ldarg_0, (object)null); val2.labels.AddRange(val.labels); val2.blocks.AddRange(val.blocks); list[num] = val2; list.Insert(num + 1, new CodeInstruction(OpCodes.Ldarg_1, (object)null)); list.Insert(num + 2, new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(SpiritFollowDistance), "GetStopDistance", (Type[])null, (Type[])null))); return list; } private static bool IsStopDistance(CodeInstruction instruction) { if (instruction.opcode == OpCodes.Ldc_R4 && instruction.operand is float num) { return num == 3f; } return false; } private static float GetStopDistance(BaseAI ai, GameObject followTarget) { Plugin instance = Plugin.Instance; Player localPlayer = Player.m_localPlayer; if ((Object)(object)instance == (Object)null || !instance.ModEnabled || (Object)(object)localPlayer == (Object)null || (Object)(object)followTarget != (Object)(object)((Component)localPlayer).gameObject || !(ai is MonsterAI)) { return 3f; } int spiritFollowDistanceMeters = instance.SpiritFollowDistanceMeters; if ((float)spiritFollowDistanceMeters <= 3f) { return 3f; } ZNetView component = ((Component)ai).GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner()) { return 3f; } ZDO zDO = component.GetZDO(); if (zDO == null || !SpiritCaller.TryGetSpecies(zDO.GetPrefab(), out var _)) { return 3f; } long playerID = localPlayer.GetPlayerID(); if (playerID == 0L || zDO.GetLong(SummonIndicator.SummonerKey, 0L) != playerID) { return 3f; } return spiritFollowDistanceMeters; } } internal sealed class CastRequest { internal readonly SummonMode Mode; internal readonly Player Caster; internal SpiritCast Spirit; internal int SelectedCount; internal CastRequest(SummonMode mode, Player caster) { Mode = mode; Caster = caster; } } internal static class SummonHooks { [HarmonyPatch(typeof(SpawnAbility), "Setup")] private static class CaptureCast { private static bool Prefix(SpawnAbility __instance, Character owner, ItemData item) { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Expected O, but got Unknown //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Expected O, but got Unknown if (requests.TryGetValue(__instance, out var value)) { SpiritCaller.Release(value.Spirit); } requests.Remove(__instance); Plugin instance = Plugin.Instance; if ((Object)(object)instance == (Object)null || !instance.ModEnabled || (Object)(object)owner == (Object)null || (Object)(object)owner != (Object)(object)Player.m_localPlayer) { return true; } ItemData val = item ?? ((Humanoid)Player.m_localPlayer).GetCurrentWeapon(); if (SpiritCaller.IsStaff(val)) { if (!SpiritCaller.Prepare(__instance, (Player)owner, val, instance.SelectedSpirit, out var cast)) { Object.Destroy((Object)(object)((Component)__instance).gameObject); return false; } if (cast != null) { requests.Add(__instance, new CastRequest(SummonMode.Random, (Player)owner) { Spirit = cast }); } return true; } if (!IsDeadRaiser(val) || !SpawnsFriendlySkeleton(__instance)) { return true; } SummonMode selectedMode = instance.SelectedMode; requests.Add(__instance, new CastRequest(selectedMode, (Player)owner)); if (instance.DebugLogging) { instance.Log.LogInfo((object)("Captured Dead Raiser cast: " + selectedMode.ToString() + "; weapon supplied=" + (item != null))); } return true; } } private struct SpawnStepState { internal CastRequest Previous; internal SpawnAbility Ability; internal GameObject[] OriginalPrefabs; } [HarmonyPatch] private static class SpawnStepScope { private static MethodBase TargetMethod() { return SpawnStep; } private static void Prefix(object __instance, out SpawnStepState __state) { __state = new SpawnStepState { Previous = current }; current = null; object? value = abilityField.GetValue(__instance); SpawnAbility val = (SpawnAbility)((value is SpawnAbility) ? value : null); if (!((Object)(object)val == (Object)null) && requests.TryGetValue(val, out var value2)) { current = value2; if (value2.Spirit != null && value2.Spirit.Prefabs != null) { __state.Ability = val; __state.OriginalPrefabs = val.m_spawnPrefab; val.m_spawnPrefab = value2.Spirit.Prefabs; } } } private static void Finalizer(Exception __exception, SpawnStepState __state) { if (__exception != null && current != null) { SpiritCaller.Release(current.Spirit); } if ((Object)(object)__state.Ability != (Object)null) { __state.Ability.m_spawnPrefab = __state.OriginalPrefabs; } current = __state.Previous; } private static void Postfix(bool __result) { CastRequest current = SummonHooks.current; if (!__result && current != null && current.Spirit != null) { SpiritCaller.Release(current.Spirit); } if (!__result && current != null && current.Mode != SummonMode.Random && current.SelectedCount == 0 && Plugin.Instance.DebugLogging) { Plugin.Instance.Log.LogWarning((object)"Dead Raiser cast finished without selecting equipment (the summon may have been blocked by its limit or spawn point)."); } } } [HarmonyPatch(typeof(Character), "Awake")] private static class TrackSpiritSpawn { [HarmonyPriority(0)] private static void Postfix(Character __instance) { CastRequest current = SummonHooks.current; if (current == null || current.Spirit == null || (Object)(object)__instance == (Object)null || __instance.IsPlayer()) { return; } ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner()) { return; } ZDO zDO = component.GetZDO(); if (SpiritCaller.TryGetSpecies(zDO.GetPrefab(), out var species) && (current.Spirit.Prefabs == null || current.Spirit.Species == species)) { SummonIndicator.MarkSummon(zDO, current.Caster); SpiritCaller.Release(current.Spirit); Plugin instance = Plugin.Instance; if (instance.DebugLogging || reportedSelections++ < 12) { instance.Log.LogInfo((object)("Summoned Spirit Caller spirit: " + species.ToString() + ".")); } } } } [HarmonyPatch(typeof(Humanoid), "Awake")] private static class SelectEquipmentSeed { [HarmonyPriority(0)] private static void Postfix(Humanoid __instance) { //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) CastRequest current = SummonHooks.current; if (current == null || current.Spirit != null || (Object)(object)__instance == (Object)null || ((Character)__instance).IsPlayer() || !((Object)((Component)__instance).gameObject).name.StartsWith("Skeleton_Friendly", StringComparison.Ordinal)) { return; } ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid() || !component.IsOwner()) { return; } SummonIndicator.MarkSummon(component.GetZDO(), current.Caster); if (current.Mode == SummonMode.Random) { return; } State state = Random.state; try { if (!SelectionPolicy.TryChoose(ReadTable(__instance), originalSeed: (int)seedField.GetValue(__instance), mode: current.Mode, random: new UnitySeedRandom(), selectedSeed: out var selectedSeed)) { Warn(current, "The summoned skeleton has no matching vanilla equipment combination."); return; } component.GetZDO().Set(ZDOVars.s_seed, selectedSeed, false); seedField.SetValue(__instance, selectedSeed); current.SelectedCount++; if (Plugin.Instance.DebugLogging || reportedSelections++ < 12) { Plugin.Instance.Log.LogInfo((object)("Selected " + current.Mode.ToString() + " for " + ((Object)((Component)__instance).gameObject).name + "; synchronized vanilla equipment seed.")); } } catch (Exception ex) { Plugin.Instance.Log.LogError((object)("Equipment selection failed; vanilla summon retained. " + ex)); Warn(current, "Could not select equipment; vanilla summon retained."); } finally { Random.state = state; } } } private sealed class UnitySeedRandom : ISeedRandom { public void Initialize(int seed) { Random.InitState(seed); } public int Index(int count) { return Random.Range(0, count); } public float Value() { return Random.value; } } private static ConditionalWeakTable<SpawnAbility, CastRequest> requests = new ConditionalWeakTable<SpawnAbility, CastRequest>(); [ThreadStatic] private static CastRequest current; private static readonly FieldInfo seedField = AccessTools.Field(typeof(Humanoid), "m_seed"); private static FieldInfo abilityField; private static int reportedSelections; internal static MethodInfo SpawnStep { get; private set; } internal static void VerifyHooks() { SpawnStep = AccessTools.EnumeratorMoveNext((MethodBase)AccessTools.Method(typeof(SpawnAbility), "Spawn", (Type[])null, (Type[])null)); if (SpawnStep == null || seedField == null) { throw new MissingMethodException("Valheim summoning hooks could not be found."); } abilityField = AccessTools.Field(SpawnStep.DeclaringType, "<>4__this"); if (abilityField == null) { throw new MissingFieldException("Spawn coroutine does not expose its owner."); } } internal static void Clear() { current = null; requests = new ConditionalWeakTable<SpawnAbility, CastRequest>(); reportedSelections = 0; SpiritCaller.Clear(); } internal static bool IsDeadRaiser(ItemData item) { if (item != null && (Object)(object)item.m_dropPrefab != (Object)null) { return ((Object)item.m_dropPrefab).name == "StaffSkeleton"; } return false; } private static bool SpawnsFriendlySkeleton(SpawnAbility ability) { if (ability.m_spawnPrefab == null) { return false; } GameObject[] spawnPrefab = ability.m_spawnPrefab; foreach (GameObject val in spawnPrefab) { if ((Object)(object)val != (Object)null && ((Object)val).name.StartsWith("Skeleton_Friendly", StringComparison.Ordinal)) { return true; } } return false; } private static void Warn(CastRequest request, string detail) { Plugin instance = Plugin.Instance; if (!((Object)(object)instance == (Object)null)) { instance.Log.LogWarning((object)detail); if ((Object)(object)request.Caster != (Object)null) { ((Character)request.Caster).Message((MessageType)2, "Skeleton type could not be selected. See the log.", 0, (Sprite)null, false); } } } private static EquipmentTable ReadTable(Humanoid humanoid) { EquipmentTable equipmentTable = new EquipmentTable { Defaults = ReadItems(humanoid.m_defaultItems), Shields = ReadItems(humanoid.m_randomShield), Weapons = ReadItems(humanoid.m_randomWeapon), Armor = ReadItems(humanoid.m_randomArmor) }; ItemSet[] array = (ItemSet[])(((object)humanoid.m_randomSets) ?? ((object)new ItemSet[0])); equipmentTable.Sets = new EquipmentItem[array.Length][]; for (int i = 0; i < array.Length; i++) { equipmentTable.Sets[i] = ReadItems(array[i].m_items); } RandomItem[] array2 = (RandomItem[])(((object)humanoid.m_randomItems) ?? ((object)new RandomItem[0])); equipmentTable.Items = new ChanceItem[array2.Length]; for (int j = 0; j < array2.Length; j++) { equipmentTable.Items[j] = new ChanceItem { Item = ReadItem(array2[j].m_prefab), Chance = array2[j].m_chance }; } return equipmentTable; } private static EquipmentItem[] ReadItems(GameObject[] prefabs) { if (prefabs == null) { return new EquipmentItem[0]; } EquipmentItem[] array = new EquipmentItem[prefabs.Length]; for (int i = 0; i < array.Length; i++) { array[i] = ReadItem(prefabs[i]); } return array; } private static EquipmentItem ReadItem(GameObject prefab) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Invalid comparison between Unknown and I4 //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Invalid comparison between Unknown and I4 //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected I4, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Invalid comparison between Unknown and I4 if ((Object)(object)prefab == (Object)null) { return null; } ItemDrop component = prefab.GetComponent<ItemDrop>(); if ((Object)(object)component == (Object)null) { throw new InvalidOperationException("Equipment prefab has no ItemDrop: " + ((Object)prefab).name); } ItemData itemData = component.m_itemData; EquipmentKind kind = EquipmentKind.Other; if ((int)itemData.m_shared.m_itemType == 5) { kind = EquipmentKind.Shield; } else if ((int)itemData.m_shared.m_itemType == 4) { kind = EquipmentKind.Bow; } else if (itemData.IsWeapon()) { kind = (((int)itemData.m_shared.m_skillType == 1) ? EquipmentKind.Sword : EquipmentKind.OtherWeapon); } return new EquipmentItem(kind, (int)itemData.m_shared.m_itemType); } } public sealed class SummonedSkeletonsEffect : StatusEffect { internal int Count; internal string Caption; public override string GetIconText() { return Count.ToString(CultureInfo.InvariantCulture); } public override string GetTooltipString() { return Caption + ": " + ((StatusEffect)this).GetIconText(); } } internal static class SummonIndicator { [HarmonyPatch(typeof(Tameable), "RPC_UnSummon")] private static class TrackUnsummon { private static void Prefix(Tameable __instance) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { ZDO zDO = component.GetZDO(); if (zDO != null && zDO.GetLong(SummonerKey, 0L) == localPlayer.GetPlayerID()) { unsummoning.Add(zDO.m_uid); nextScan = 0f; } } } } [HarmonyPatch(typeof(SEMan), "GetHUDStatusEffects")] private static class AddIndicator { private static void Postfix(SEMan __instance, List<StatusEffect> effects) { Plugin instance = Plugin.Instance; Player localPlayer = Player.m_localPlayer; if ((Object)(object)instance == (Object)null || (Object)(object)localPlayer == (Object)null || (Object)(object)localPlayer != (Object)(object)player || world != ZDOMan.instance || __instance != ((Character)localPlayer).GetSEMan()) { return; } bool localPlayerAlive = !((Character)localPlayer).IsDead(); if (SummonCountPolicy.Visible(instance.ModEnabled, instance.ShowSummonBuff, localPlayerAlive, count)) { SummonedSkeletonsEffect effect = GetEffect(ref SummonIndicator.effect, "Skeletons", "StaffSkeleton", count); if ((Object)(object)((StatusEffect)effect).m_icon != (Object)null && !effects.Contains((StatusEffect)(object)effect)) { effects.Add((StatusEffect)(object)effect); } } if (SummonCountPolicy.Visible(instance.ModEnabled, instance.ShowSpiritBuff, localPlayerAlive, spiritCount)) { SummonedSkeletonsEffect effect2 = GetEffect(ref spiritEffect, "Spirits", "StaffSpiritCaller", spiritCount); if ((Object)(object)((StatusEffect)effect2).m_icon != (Object)null && !effects.Contains((StatusEffect)(object)effect2)) { effects.Add((StatusEffect)(object)effect2); } } } } internal static readonly int SummonerKey = StringExtensionMethods.GetStableHashCode("Slikfoul.SkeletonChoice.Summoner"); private static readonly List<SummonSnapshot> snapshots = new List<SummonSnapshot>(); private static readonly List<SummonSnapshot> spiritSnapshots = new List<SummonSnapshot>(); private static readonly HashSet<ZDOID> unsummoning = new HashSet<ZDOID>(); private static readonly HashSet<ZDOID> loadedIds = new HashSet<ZDOID>(); private static SummonedSkeletonsEffect effect; private static SummonedSkeletonsEffect spiritEffect; private static Player player; private static ZDOMan world; private static float nextScan; private static int count; private static int spiritCount; internal static void MarkSummon(ZDO zdo, Player caster) { if (zdo != null && !((Object)(object)caster == (Object)null)) { long playerID = caster.GetPlayerID(); if (playerID != 0L) { zdo.Set(SummonerKey, playerID); nextScan = 0f; } } } internal static void Update() { //IL_012c: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)(object)player || world != ZDOMan.instance) { count = 0; spiritCount = 0; snapshots.Clear(); spiritSnapshots.Clear(); unsummoning.Clear(); loadedIds.Clear(); nextScan = 0f; player = localPlayer; world = ZDOMan.instance; } Plugin instance = Plugin.Instance; if ((Object)(object)instance == (Object)null || !instance.ModEnabled || (!instance.ShowSummonBuff && !instance.ShowSpiritBuff) || (Object)(object)localPlayer == (Object)null || ((Character)localPlayer).IsDead() || world == null) { count = 0; spiritCount = 0; nextScan = 0f; } else { if (Time.unscaledTime < nextScan) { return; } nextScan = Time.unscaledTime + 0.5f; long playerID = localPlayer.GetPlayerID(); snapshots.Clear(); spiritSnapshots.Clear(); loadedIds.Clear(); foreach (Character allCharacter in Character.GetAllCharacters()) { if (TryRead(allCharacter, playerID, out var zdo, out var snapshot)) { loadedIds.Add(zdo.m_uid); if (SpiritCaller.TryGetSpecies(zdo.GetPrefab(), out var _)) { spiritSnapshots.Add(snapshot); } else { snapshots.Add(snapshot); } } } unsummoning.IntersectWith(loadedIds); count = SummonCountPolicy.Count(playerID, snapshots); spiritCount = SummonCountPolicy.Count(playerID, spiritSnapshots); } } internal static void CollectSpirits(Player caster, IDictionary<SpiritMode, int> occupied) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) long playerID = caster.GetPlayerID(); HashSet<ZDOID> hashSet = new HashSet<ZDOID>(); foreach (Character allCharacter in Character.GetAllCharacters()) { if (TryRead(allCharacter, playerID, out var zdo, out var snapshot) && SpiritCaller.TryGetSpecies(zdo.GetPrefab(), out var species) && SummonCountPolicy.IsActive(playerID, snapshot) && hashSet.Add(zdo.m_uid)) { SpiritSelectionPolicy.Add(occupied, species); } } } private static bool TryRead(Character character, long playerId, out ZDO zdo, out SummonSnapshot snapshot) { //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) zdo = null; snapshot = null; if (playerId == 0L || (Object)(object)character == (Object)null || character.IsPlayer() || !((Component)character).gameObject.activeInHierarchy) { return false; } ZNetView component = ((Component)character).GetComponent<ZNetView>(); if ((Object)(object)component == (Object)null || !component.IsValid()) { return false; } zdo = component.GetZDO(); if (zdo == null || !zdo.IsValid() || zdo.GetLong(SummonerKey, 0L) != playerId) { return false; } Tameable component2 = ((Component)character).GetComponent<Tameable>(); MonsterAI component3 = ((Component)character).GetComponent<MonsterAI>(); GameObject val = (((Object)(object)component3 != (Object)null) ? component3.GetFollowTarget() : null); bool flag = (Object)(object)val != (Object)null; float followDistance = (flag ? Vector3.Distance(val.transform.position, ((Component)character).transform.position) : 0f); snapshot = new SummonSnapshot(((object)Unsafe.As<ZDOID, ZDOID>(ref zdo.m_uid)/*cast due to .constrained prefix*/).ToString(), playerId, exists: true, character.IsDead() ? 0f : character.GetHealth(), loaded: true, unsummoning.Contains(zdo.m_uid), flag, followDistance, ((Object)(object)component2 != (Object)null) ? component2.m_unsummonDistance : 0f); return true; } private static SummonedSkeletonsEffect GetEffect(ref SummonedSkeletonsEffect status, string title, string staffName, int amount) { if ((Object)(object)status == (Object)null) { status = ScriptableObject.CreateInstance<SummonedSkeletonsEffect>(); ((Object)status).name = "SkeletonChoice_Summoned" + title; ((StatusEffect)status).m_name = title; status.Caption = "Your summoned " + title.ToLowerInvariant(); ((StatusEffect)status).m_ttl = 0f; ((StatusEffect)status).m_isNew = false; } if ((Object)(object)((StatusEffect)status).m_icon == (Object)null && (Object)(object)ObjectDB.instance != (Object)null) { GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(staffName); ItemDrop val = (((Object)(object)itemPrefab != (Object)null) ? itemPrefab.GetComponent<ItemDrop>() : null); if ((Object)(object)val != (Object)null && val.m_itemData.m_shared.m_icons.Length != 0) { ((StatusEffect)status).m_icon = val.m_itemData.m_shared.m_icons[0]; } } status.Count = amount; return status; } internal static void Clear() { if ((Object)(object)effect != (Object)null) { Object.Destroy((Object)(object)effect); } if ((Object)(object)spiritEffect != (Object)null) { Object.Destroy((Object)(object)spiritEffect); } effect = null; spiritEffect = null; player = null; world = null; nextScan = 0f; count = 0; spiritCount = 0; snapshots.Clear(); spiritSnapshots.Clear(); unsummoning.Clear(); loadedIds.Clear(); } } }