Decompiled source of BloodMagicCompanion v1.4.1

plugins/BloodMagicCompanion.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("1.4.1.0")]
namespace BloodMagicCompanion;

internal static class CasterXP
{
	private sealed class Credit
	{
		internal ZDOID Caster;

		internal string Token = Guid.NewGuid().ToString("N");

		internal bool Consumed;
	}

	[HarmonyPatch(typeof(Player), "Awake")]
	private static class AdvertiseProtocol
	{
		private static void Postfix(Player __instance)
		{
			Advertise(__instance);
		}
	}

	[HarmonyPatch(/*Could not decode attribute arguments.*/)]
	private static class RegisterProtocol
	{
		private static void Postfix(ZRoutedRpc __instance)
		{
			credits = new ConditionalWeakTable<SE_Shield, Credit>();
			receipts.Clear();
			__instance.Register<ZPackage>("BloodMagicCasterXP_Break_v3", (Action<long, ZPackage>)ReceiveBreak);
		}
	}

	[HarmonyPatch(typeof(Character), "RPC_Damage")]
	private static class CaptureCaster
	{
		internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			return ReplaceCall(instructions, AccessTools.Method(typeof(StatusEffect), "SetAttacker", (Type[])null, (Type[])null), AccessTools.Method(typeof(CasterXP), "RememberCaster", (Type[])null, (Type[])null), appendInstance: false);
		}
	}

	[HarmonyPatch(typeof(SE_Shield), "IsDone")]
	private static class RedirectReward
	{
		internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			return ReplaceCall(instructions, AccessTools.Method(typeof(Character), "GetSkills", (Type[])null, (Type[])null), AccessTools.Method(typeof(CasterXP), "GetRewardSkills", (Type[])null, (Type[])null), appendInstance: true);
		}
	}

	private const string LegacyId = "local.valheim.bloodmagiccasterxp";

	internal const string RpcName = "BloodMagicCasterXP_Break_v3";

	internal static readonly int ShieldHash = StringExtensionMethods.GetStableHashCode("Staff_shield");

	internal static readonly int ProtocolHash = StringExtensionMethods.GetStableHashCode("local.valheim.bloodmagiccasterxp.protocol");

	private static ConditionalWeakTable<SE_Shield, Credit> credits = new ConditionalWeakTable<SE_Shield, Credit>();

	private static readonly ReceiptLedger receipts = new ReceiptLedger(4096);

	private static ManualLogSource log;

	private static ConfigEntry<bool> diagnostics;

	private static bool ready;

	internal static void Configure(ConfigFile config, ManualLogSource logger)
	{
		log = logger;
		diagnostics = config.Bind<bool>("Logging", "Diagnostics", false, "Log shield attribution, Trollstav tracking and XP delivery for multiplayer troubleshooting.");
	}

	internal static void Patch(Harmony harmony)
	{
		Type[] array = new Type[4]
		{
			typeof(AdvertiseProtocol),
			typeof(RegisterProtocol),
			typeof(CaptureCaster),
			typeof(RedirectReward)
		};
		foreach (Type type in array)
		{
			harmony.CreateClassProcessor(type).Patch();
		}
	}

	internal static void SetEnabled(bool enabled)
	{
		ready = enabled;
		Advertise(Player.m_localPlayer);
	}

	internal static void Advertise(Player player)
	{
		if (!((Object)(object)player == (Object)null))
		{
			ZNetView component = ((Component)player).GetComponent<ZNetView>();
			int num = (ready ? 3 : 0);
			if ((Object)(object)component != (Object)null && component.IsValid() && component.IsOwner() && component.GetZDO().GetInt(ProtocolHash, 0) != num)
			{
				component.GetZDO().Set(ProtocolHash, num, false);
			}
		}
	}

	internal static void Trace(string message)
	{
		if (diagnostics != null && diagnostics.Value)
		{
			log.LogInfo((object)message);
		}
	}

	internal static void RememberCaster(StatusEffect effect, Character attacker)
	{
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_0076: Unknown result type (might be due to invalid IL or missing references)
		//IL_0087: Unknown result type (might be due to invalid IL or missing references)
		effect.SetAttacker(attacker);
		if (!ready)
		{
			return;
		}
		SE_Shield val = (SE_Shield)(object)((effect is SE_Shield) ? effect : null);
		if ((Object)(object)val == (Object)null || ((StatusEffect)val).NameHash() != ShieldHash)
		{
			return;
		}
		credits.Remove(val);
		Player val2 = (Player)(object)((attacker is Player) ? attacker : null);
		if (!((Object)(object)val2 == (Object)null))
		{
			ZNetView component = ((Component)val2).GetComponent<ZNetView>();
			if (!((Object)(object)component == (Object)null) && component.IsValid())
			{
				credits.Add(val, new Credit
				{
					Caster = ((Character)val2).GetZDOID()
				});
				Trace("Tracked shield caster " + ((Character)val2).GetZDOID());
			}
		}
	}

	internal static Skills GetRewardSkills(Character protectedCharacter, SE_Shield shield)
	{
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0023: Invalid comparison between Unknown and I4
		//IL_0053: Unknown result type (might be due to invalid IL or missing references)
		//IL_0059: Unknown result type (might be due to invalid IL or missing references)
		//IL_0074: Unknown result type (might be due to invalid IL or missing references)
		//IL_0105: Unknown result type (might be due to invalid IL or missing references)
		//IL_010c: Expected O, but got Unknown
		//IL_010f: Unknown result type (might be due to invalid IL or missing references)
		//IL_011c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0164: Unknown result type (might be due to invalid IL or missing references)
		if (!ready)
		{
			return protectedCharacter.GetSkills();
		}
		if (((StatusEffect)shield).NameHash() != ShieldHash || (int)shield.m_levelUpSkillOnBreak != 10 || !credits.TryGetValue(shield, out var value))
		{
			return protectedCharacter.GetSkills();
		}
		Player localPlayer = Player.m_localPlayer;
		bool casterIsLocal = (Object)(object)localPlayer != (Object)null && ((Character)localPlayer).IsOwner() && ((Character)localPlayer).GetZDOID() == value.Caster;
		ZDO val = ((ZDOMan.instance == null) ? null : ZDOMan.instance.GetZDO(value.Caster));
		int casterProtocol = ((val != null) ? val.GetInt(ProtocolHash, 0) : 0);
		long num = ((val == null) ? 0 : val.GetOwner());
		ZRoutedRpc instance = ZRoutedRpc.instance;
		RewardRoute rewardRoute = RewardRouting.Select(protectedCharacter.IsOwner(), value.Consumed, casterIsLocal, casterProtocol, num, instance != null);
		if (rewardRoute == RewardRoute.Suppress)
		{
			return null;
		}
		value.Consumed = true;
		switch (rewardRoute)
		{
		case RewardRoute.Vanilla:
			Trace("Caster unavailable or incompatible; using vanilla shield reward.");
			return protectedCharacter.GetSkills();
		case RewardRoute.LocalCaster:
			Trace("Shield broke; XP awarded directly to local caster.");
			return ((Character)localPlayer).GetSkills();
		default:
		{
			ZPackage val2 = new ZPackage();
			val2.Write(value.Caster);
			val2.Write(protectedCharacter.GetZDOID());
			val2.Write(value.Token);
			val2.Write(shield.m_levelUpSkillFactor);
			instance.InvokeRoutedRPC(num, "BloodMagicCasterXP_Break_v3", new object[1] { val2 });
			Trace(string.Concat("Redirected shield-break XP to ", value.Caster, " (recipient receives none)."));
			return null;
		}
		}
	}

	internal static void ReceiveBreak(long sender, ZPackage packet)
	{
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0031: Unknown result type (might be due to invalid IL or missing references)
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Unknown result type (might be due to invalid IL or missing references)
		//IL_0062: Unknown result type (might be due to invalid IL or missing references)
		if (!ready)
		{
			return;
		}
		try
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || !((Character)localPlayer).IsOwner())
			{
				return;
			}
			ZDOID val = packet.ReadZDOID();
			ZDOID val2 = packet.ReadZDOID();
			string token = packet.ReadString();
			float num = packet.ReadSingle();
			if (val != ((Character)localPlayer).GetZDOID() || val2 == val)
			{
				return;
			}
			ZDO zDO = ZDOMan.instance.GetZDO(val2);
			if (ReceiptLedger.IsAuthorizedSender(sender, (zDO == null) ? ((long?)null) : new long?(zDO.GetOwner())))
			{
				Skills skills = ((Character)localPlayer).GetSkills();
				if (!((Object)(object)skills == (Object)null) && receipts.Accept(sender, token, num))
				{
					skills.RaiseSkill((SkillType)10, num);
					Trace("Received shield-break Blood Magic XP, factor " + num + ".");
				}
			}
		}
		catch (Exception ex)
		{
			log.LogWarning((object)("Ignored invalid shield XP message: " + ex.Message));
		}
	}

	internal static IEnumerable<CodeInstruction> ReplaceCall(IEnumerable<CodeInstruction> instructions, MethodInfo original, MethodInfo replacement, bool appendInstance)
	{
		//IL_003c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0042: Expected O, but got Unknown
		List<CodeInstruction> list = new List<CodeInstruction>();
		int num = 0;
		foreach (CodeInstruction instruction in instructions)
		{
			if (!CodeInstructionExtensions.Calls(instruction, original))
			{
				list.Add(instruction);
				continue;
			}
			num++;
			if (appendInstance)
			{
				CodeInstruction val = new CodeInstruction(OpCodes.Ldarg_0, (object)null);
				val.labels.AddRange(instruction.labels);
				val.blocks.AddRange(instruction.blocks);
				instruction.labels.Clear();
				instruction.blocks.Clear();
				list.Add(val);
			}
			instruction.opcode = OpCodes.Call;
			instruction.operand = replacement;
			list.Add(instruction);
		}
		if (num != 1)
		{
			throw new InvalidOperationException(string.Concat("Expected exactly one ", original, "; found ", num));
		}
		return list;
	}
}
internal static class CostModel
{
	internal static bool Finite(float value)
	{
		if (!float.IsNaN(value))
		{
			return !float.IsInfinity(value);
		}
		return false;
	}

	internal static float Surcharge(float eitr, float skill, float rate, float reduction, float item)
	{
		if (!Finite(eitr) || !Finite(skill) || !Finite(rate) || !Finite(reduction) || !Finite(item) || eitr < 0f || rate <= 0f || item <= 0f || reduction < 0f || reduction >= 1f)
		{
			return float.MaxValue;
		}
		double num = 1.0 - (double)reduction * Math.Max(0.0, Math.Min(1.0, skill));
		double val = (double)eitr * (double)rate * (double)item * num;
		return (float)Math.Min(3.4028234663852886E+38, val);
	}

	internal static float Total(float nativeStamina, float surcharge)
	{
		if (!Finite(nativeStamina) || !Finite(surcharge) || surcharge < 0f)
		{
			return float.MaxValue;
		}
		return (float)Math.Min(3.4028234663852886E+38, Math.Max(0.0, nativeStamina) + (double)surcharge);
	}

	internal static float Setting(float value, float minimum, float maximum, float fallback)
	{
		if (!Finite(value) || !(value >= minimum) || !(value <= maximum))
		{
			return fallback;
		}
		return value;
	}
}
[BepInIncompatibility("local.valheim.bloodmagiccasterxp")]
[BepInPlugin("local.valheim.bloodmagiccompanion", "Blood Magic Companion", "1.4.1")]
[BepInIncompatibility("local.valheim.bloodmagichealthcost")]
public sealed class Plugin : BaseUnityPlugin
{
	public const string Id = "local.valheim.bloodmagiccompanion";

	public const string Version = "1.4.1";

	private Harmony harmony;

	private void Awake()
	{
		//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
		//IL_0105: Expected O, but got Unknown
		bool value = ((BaseUnityPlugin)this).Config.Bind<bool>("HealthCosts", "Enabled", true, "Deprecated: only seeds StaminaCosts/Enabled if the new entry does not exist. Other old HealthCosts settings are ignored.").Value;
		bool value2 = ((BaseUnityPlugin)this).Config.Bind<bool>("StaminaCosts", "Enabled", value, "Replace Blood Magic eitr with stamina; keep original health costs. Restart required.").Value;
		bool value3 = ((BaseUnityPlugin)this).Config.Bind<bool>("CasterXP", "Enabled", true, "Redirect shield-break XP to the caster when the simulation owner and caster support it. Restart required.").Value;
		bool value4 = ((BaseUnityPlugin)this).Config.Bind<bool>("Summons", "AttackTrainingDummy", false, "Allow owned summoned skeletons to attack T.W.I.G. when no ordinary enemy is available. The skeleton simulation owner needs this enabled. Restart required.").Value;
		bool value5 = ((BaseUnityPlugin)this).Config.Bind<bool>("Summons", "LavaDamageImmunity", false, "Protect owned summoned skeletons from lava damage and lava-applied burning. Other fire damage and boiling ocean damage remain. The simulation owner needs this enabled. Restart required.").Value;
		bool value6 = ((BaseUnityPlugin)this).Config.Bind<bool>("TrollXP", "Enabled", false, "Award caster Blood Magic XP for summoned troll damage to enemies. Caster and victim simulation owner need this enabled. Resummon trolls after upgrading. Restart required.").Value;
		TrollXP.Configure(((BaseUnityPlugin)this).Config);
		StaminaCosts.Configure(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
		CasterXP.Configure(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
		harmony = new Harmony("local.valheim.bloodmagiccompanion");
		try
		{
			if (value2)
			{
				StaminaCosts.Patch(harmony);
			}
			if (value3)
			{
				CasterXP.Patch(harmony);
			}
			if (value4)
			{
				SummonTraining.Patch(harmony);
			}
			if (value5)
			{
				SummonLavaProtection.Patch(harmony);
			}
			if (value6)
			{
				TrollXP.Patch(harmony);
			}
			StaminaCosts.Active = value2;
			SummonTraining.Active = value4;
			SummonLavaProtection.Active = value5;
			CasterXP.SetEnabled(value3);
			TrollXP.SetEnabled(value6);
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Blood Magic Companion 1.4.1 loaded. StaminaCosts=" + value2 + "; CasterXP=" + value3 + "; AttackTrainingDummy=" + value4 + "; LavaDamageImmunity=" + value5 + "; TrollXP=" + value6 + ". Server and other clients are optional."));
		}
		catch (Exception ex)
		{
			StaminaCosts.Active = false;
			SummonTraining.Active = false;
			SummonLavaProtection.Active = false;
			CasterXP.SetEnabled(enabled: false);
			TrollXP.SetEnabled(enabled: false);
			harmony.UnpatchSelf();
			((BaseUnityPlugin)this).Logger.LogError((object)("Could not install all requested patches; vanilla behavior retained. " + ex));
		}
	}

	private void Update()
	{
		CasterXP.Advertise(Player.m_localPlayer);
		TrollXP.Advertise(Player.m_localPlayer);
	}

	private void OnDestroy()
	{
		StaminaCosts.Active = false;
		SummonTraining.Active = false;
		SummonLavaProtection.Active = false;
		CasterXP.SetEnabled(enabled: false);
		TrollXP.SetEnabled(enabled: false);
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
	}
}
internal sealed class ReceiptLedger
{
	private readonly int capacity;

	private readonly HashSet<string> seen = new HashSet<string>();

	private readonly Queue<string> order = new Queue<string>();

	internal ReceiptLedger(int capacity)
	{
		this.capacity = capacity;
	}

	internal void Clear()
	{
		seen.Clear();
		order.Clear();
	}

	internal static bool IsAuthorizedSender(long sender, long? currentOwner)
	{
		if (sender != 0 && currentOwner.HasValue)
		{
			return currentOwner.Value == sender;
		}
		return false;
	}

	internal bool Accept(long sender, string token, float factor)
	{
		if (sender == 0 || token == null || token.Length != 32 || !Guid.TryParseExact(token, "N", out var _) || float.IsNaN(factor) || float.IsInfinity(factor) || factor <= 0f || factor > 100f)
		{
			return false;
		}
		string item = sender.ToString(CultureInfo.InvariantCulture) + ":" + token;
		if (!seen.Add(item))
		{
			return false;
		}
		order.Enqueue(item);
		while (order.Count > capacity)
		{
			seen.Remove(order.Dequeue());
		}
		return true;
	}
}
internal enum RewardRoute
{
	Vanilla,
	Suppress,
	LocalCaster,
	RemoteCaster
}
internal static class RewardRouting
{
	internal const int Protocol = 3;

	internal static RewardRoute Select(bool ownsRecipient, bool consumed, bool casterIsLocal, int casterProtocol, long casterPeer, bool routerAvailable)
	{
		if (!ownsRecipient || consumed)
		{
			return RewardRoute.Suppress;
		}
		if (casterIsLocal)
		{
			return RewardRoute.LocalCaster;
		}
		if (casterProtocol == 3 && casterPeer != 0 && routerAvailable)
		{
			return RewardRoute.RemoteCaster;
		}
		return RewardRoute.Vanilla;
	}
}
internal static class StaminaCosts
{
	internal static bool Active;

	internal static bool Tooltips;

	internal static float EitrRate;

	internal static float SkillReduction;

	private static readonly Dictionary<string, float> Multipliers = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);

	private static ConfigFile config;

	private static ManualLogSource log;

	internal static void Configure(ConfigFile settings, ManualLogSource logger)
	{
		config = settings;
		log = logger;
		Multipliers.Clear();
		Tooltips = config.Bind<bool>("StaminaCosts", "ShowCostInTooltip", true, "Show the additional stamina converted from eitr. Original health costs remain unchanged. Restart required.").Value;
		EitrRate = ReadNumber("StaminaPerEitr", 1f, 0.01f, 100f, "Stamina per BASE eitr point, before the Blood Magic skill discount. This is a fixed amount, not a fraction of remaining stamina.");
		SkillReduction = ReadNumber("CostReductionAtSkill100", 0.33f, 0f, 0.9f, "Discount on converted stamina at Blood Magic level 100. Applied once; 0.33 means 33%. Does not change vanilla health or native stamina costs.");
		string value = config.Bind<string>("StaminaCosts", "ItemCostMultipliers", "StaffShield=1;StaffSkeleton=1;StaffTroll=1", "Prefab-name=multiplier entries separated by semicolons. Applies only to converted stamina. Unknown items use 1; range 0.01 to 100; last valid duplicate wins. Restart required.").Value;
		string[] array = value.Split(';');
		foreach (string text in array)
		{
			if (!string.IsNullOrWhiteSpace(text))
			{
				string[] array2 = text.Split('=');
				if (array2.Length != 2 || string.IsNullOrWhiteSpace(array2[0]) || !float.TryParse(array2[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || !CostModel.Finite(result) || result < 0.01f || result > 100f)
				{
					log.LogWarning((object)("Ignoring invalid ItemCostMultipliers entry: " + text));
				}
				else
				{
					Multipliers[array2[0].Trim()] = result;
				}
			}
		}
	}

	internal static void Patch(Harmony harmony)
	{
		Type[] array = new Type[4]
		{
			typeof(EitrCostPatch),
			typeof(TooltipEitrPatch),
			typeof(StaminaCostPatch),
			typeof(CostTooltipPatch)
		};
		foreach (Type type in array)
		{
			harmony.CreateClassProcessor(type).Patch();
		}
	}

	private static float ReadNumber(string key, float fallback, float minimum, float maximum, string description)
	{
		float value = config.Bind<float>("StaminaCosts", key, fallback, description + " Valid range: " + minimum.ToString(CultureInfo.InvariantCulture) + " to " + maximum.ToString(CultureInfo.InvariantCulture) + ". Restart required.").Value;
		float num = CostModel.Setting(value, minimum, maximum, fallback);
		if (num != value)
		{
			log.LogWarning((object)("Invalid " + key + "; using " + fallback.ToString(CultureInfo.InvariantCulture)));
		}
		return num;
	}

	internal static bool Applies(Attack attack, Character character, ItemData weapon)
	{
		//IL_0039: Unknown result type (might be due to invalid IL or missing references)
		//IL_0040: Invalid comparison between Unknown and I4
		if (Active && attack != null && (Object)(object)character != (Object)null && (Object)(object)character == (Object)(object)Player.m_localPlayer && character.IsOwner() && weapon != null && weapon.m_shared != null && (int)weapon.m_shared.m_skillType == 10 && attack.m_attackEitr > 0f)
		{
			return !attack.m_attackKillsSelf;
		}
		return false;
	}

	internal static float Surcharge(Attack attack, Character character, ItemData weapon)
	{
		float value = 1f;
		if ((Object)(object)weapon.m_dropPrefab != (Object)null)
		{
			Multipliers.TryGetValue(((Object)weapon.m_dropPrefab).name, out value);
		}
		if (value <= 0f)
		{
			value = 1f;
		}
		return CostModel.Surcharge(attack.m_attackEitr, character.GetSkillFactor((SkillType)10), EitrRate, SkillReduction, value);
	}
}
[HarmonyPatch(typeof(Attack), "GetAttackEitr", new Type[] { })]
internal static class EitrCostPatch
{
	private static bool Prefix(Attack __instance, Humanoid ___m_character, ItemData ___m_weapon, ref float __result)
	{
		if (!StaminaCosts.Applies(__instance, (Character)(object)___m_character, ___m_weapon))
		{
			return true;
		}
		__result = 0f;
		return false;
	}
}
[HarmonyPatch(typeof(Attack), "GetAttackEitr", new Type[]
{
	typeof(Character),
	typeof(ItemData)
})]
internal static class TooltipEitrPatch
{
	private static bool Prefix(Attack __instance, Character __0, ItemData __1, ref float __result)
	{
		if (!StaminaCosts.Applies(__instance, __0, __1))
		{
			return true;
		}
		__result = 0f;
		return false;
	}
}
[HarmonyPatch(typeof(Attack), "GetAttackStamina", new Type[] { })]
internal static class StaminaCostPatch
{
	private static void Postfix(Attack __instance, Humanoid ___m_character, ItemData ___m_weapon, ref float __result)
	{
		if (StaminaCosts.Applies(__instance, (Character)(object)___m_character, ___m_weapon))
		{
			__result = CostModel.Total(__result, StaminaCosts.Surcharge(__instance, (Character)(object)___m_character, ___m_weapon));
		}
	}
}
[HarmonyPatch(typeof(ItemData), "GetTooltip", new Type[]
{
	typeof(ItemData),
	typeof(int),
	typeof(bool),
	typeof(float),
	typeof(int),
	typeof(bool)
})]
internal static class CostTooltipPatch
{
	private static void Postfix(ItemData __0, ref string __result)
	{
		if (StaminaCosts.Active && StaminaCosts.Tooltips && __0 != null && __0.m_shared != null && !((Object)(object)Player.m_localPlayer == (Object)null))
		{
			string text = Line("Primary", __0.m_shared.m_attack, __0);
			string text2 = Line("Secondary", __0.m_shared.m_secondaryAttack, __0);
			if (text.Length + text2.Length != 0)
			{
				string text3 = __result;
				__result = text3 + "\n\n<color=orange>Stamina-powered Blood Magic</color>" + text + text2 + "\nAdded to any native stamina cost. Original health cost unchanged. Enough stamina is required.";
			}
		}
	}

	private static string Line(string label, Attack attack, ItemData item)
	{
		if (!StaminaCosts.Applies(attack, (Character)(object)Player.m_localPlayer, item))
		{
			return "";
		}
		float num = StaminaCosts.Surcharge(attack, (Character)(object)Player.m_localPlayer, item);
		if (num == float.MaxValue)
		{
			return "\n" + label + ": unavailable (invalid or excessive cost)";
		}
		return "\n" + label + ": <color=orange>+" + num.ToString("0.00", CultureInfo.InvariantCulture) + " stamina</color> from eitr, 0 eitr. Includes your Blood Magic skill.";
	}
}
internal static class SummonLavaProtection
{
	[HarmonyPatch(typeof(Character), "UpdateHeatDamage", new Type[] { typeof(float) })]
	internal static class HeatDamagePatch
	{
		internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
		{
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01af: Expected O, but got Unknown
			//IL_0206: Unknown result type (might be due to invalid IL or missing references)
			//IL_020c: Expected O, but got Unknown
			//IL_021b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0221: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			FieldInfo fieldInfo = AccessTools.Field(typeof(Character), "m_lavaHeatLevel");
			FieldInfo fieldInfo2 = AccessTools.Field(typeof(Character), "m_ashlandsOceanHeatLevel");
			int num = -1;
			int num2 = -1;
			int num3 = 0;
			int num4 = 0;
			for (int i = 0; i < list.Count; i++)
			{
				if (list[i].blocks.Count != 0)
				{
					throw new InvalidOperationException("Unsupported heat-damage exception layout.");
				}
				if (list[i].opcode != OpCodes.Ldfld)
				{
					continue;
				}
				if (object.Equals(list[i].operand, fieldInfo))
				{
					num3++;
					if (num < 0)
					{
						num = i - 1;
					}
				}
				if (object.Equals(list[i].operand, fieldInfo2))
				{
					num4++;
					num2 = i - 1;
				}
			}
			if (fieldInfo == null || fieldInfo2 == null || num3 != 2 || num4 != 1 || num < 0 || num2 <= num || list[num].opcode != OpCodes.Ldarg_0 || list[num2].opcode != OpCodes.Ldarg_0)
			{
				throw new InvalidOperationException("Expected lava and ocean damage branches were not found.");
			}
			for (int j = num2 + 1; j < list.Count; j++)
			{
				if (list[j].opcode == OpCodes.Ldfld && object.Equals(list[j].operand, fieldInfo))
				{
					throw new InvalidOperationException("Unexpected lava branch after ocean heat.");
				}
			}
			Label label = generator.DefineLabel();
			list[num2].labels.Add(label);
			CodeInstruction val = new CodeInstruction(OpCodes.Ldarg_0, (object)null);
			val.labels.AddRange(list[num].labels);
			list[num].labels.Clear();
			list.InsertRange(num, (IEnumerable<CodeInstruction>)(object)new CodeInstruction[3]
			{
				val,
				new CodeInstruction(OpCodes.Call, (object)AccessTools.Method(typeof(SummonLavaProtection), "Applies", (Type[])null, (Type[])null)),
				new CodeInstruction(OpCodes.Brtrue, (object)label)
			});
			return list;
		}
	}

	internal static bool Active;

	internal static bool Applies(Character character)
	{
		//IL_0051: Unknown result type (might be due to invalid IL or missing references)
		//IL_0057: Invalid comparison between Unknown and I4
		if (!Active || (Object)(object)character == (Object)null)
		{
			return false;
		}
		string name = ((Object)((Component)character).gameObject).name;
		if ((name == "Skeleton_Friendly" || name == "Skeleton_Friendly(Clone)") && character.IsOwner() && character.IsTamed() && !character.IsDead())
		{
			return (int)character.GetFaction() == 0;
		}
		return false;
	}

	internal static void Patch(Harmony harmony)
	{
		harmony.CreateClassProcessor(typeof(HeatDamagePatch)).Patch();
	}
}
internal static class SummonTraining
{
	[HarmonyPatch(typeof(BaseAI), "FindEnemy", new Type[] { })]
	internal static class FindEnemyPatch
	{
		private static void Postfix(BaseAI __instance, Character ___m_character, ref Character __result)
		{
			__result = SelectTarget(__instance, ___m_character, __result);
		}
	}

	[HarmonyPatch(typeof(BaseAI), "CanSeeTarget", new Type[]
	{
		typeof(Transform),
		typeof(Vector3),
		typeof(float),
		typeof(float),
		typeof(bool),
		typeof(bool),
		typeof(Character)
	})]
	internal static class VisibilityPatch
	{
		internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_0096: Expected O, but got Unknown
			//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
			//IL_0101: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			int num = -1;
			for (int i = 0; i < list.Count; i++)
			{
				if (!(list[i].opcode != OpCodes.Call) && object.Equals(list[i].operand, RaycastMethod))
				{
					if (num >= 0 || list[i].blocks.Count != 0)
					{
						throw new InvalidOperationException("Unsupported BaseAI visibility raycast layout.");
					}
					num = i;
				}
			}
			if (RaycastMethod == null || num < 0)
			{
				throw new InvalidOperationException("Expected BaseAI visibility raycast was not found.");
			}
			CodeInstruction val = new CodeInstruction(OpCodes.Ldarg_0, (object)null);
			val.labels.AddRange(list[num].labels);
			list[num].labels.Clear();
			list[num].operand = AccessTools.Method(typeof(SummonTraining), "SightBlocked", (Type[])null, (Type[])null);
			list.Insert(num, val);
			list.Insert(num + 1, new CodeInstruction(OpCodes.Ldarg_S, (object)(byte)6));
			return list;
		}
	}

	internal static bool Active;

	internal static readonly MethodInfo RaycastMethod = AccessTools.Method(typeof(Physics), "Raycast", new Type[4]
	{
		typeof(Vector3),
		typeof(Vector3),
		typeof(float),
		typeof(int)
	}, (Type[])null);

	internal static void Patch(Harmony harmony)
	{
		harmony.CreateClassProcessor(typeof(FindEnemyPatch)).Patch();
		harmony.CreateClassProcessor(typeof(VisibilityPatch)).Patch();
	}

	private static bool Prefab(Character character, string name)
	{
		if ((Object)(object)character != (Object)null)
		{
			if (!(((Object)((Component)character).gameObject).name == name))
			{
				return ((Object)((Component)character).gameObject).name == name + "(Clone)";
			}
			return true;
		}
		return false;
	}

	internal static bool Eligible(Character character)
	{
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: Invalid comparison between Unknown and I4
		if (Active && Prefab(character, "Skeleton_Friendly") && character.IsOwner() && character.IsTamed() && !character.IsDead())
		{
			return (int)character.GetFaction() == 0;
		}
		return false;
	}

	internal static bool IsDummy(Character character)
	{
		//IL_000e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0015: Invalid comparison between Unknown and I4
		if (Prefab(character, "piece_TrainingDummy"))
		{
			return (int)character.GetFaction() == 12;
		}
		return false;
	}

	internal static Character SelectTarget(BaseAI ai, Character character, Character original)
	{
		//IL_0072: Unknown result type (might be due to invalid IL or missing references)
		//IL_007d: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)original != (Object)null || (Object)(object)ai == (Object)null || !Eligible(character))
		{
			return original;
		}
		Character targetCreature = ai.GetTargetCreature();
		if ((Object)(object)targetCreature != (Object)null && !targetCreature.IsDead() && !IsDummy(targetCreature))
		{
			return original;
		}
		Character result = null;
		float num = float.PositiveInfinity;
		foreach (Character allCharacter in Character.GetAllCharacters())
		{
			if (IsDummy(allCharacter) && !allCharacter.IsDead())
			{
				float num2 = Vector3.Distance(((Component)ai).transform.position, ((Component)allCharacter).transform.position);
				if (!(num2 >= num) && ai.CanSeeTarget(allCharacter))
				{
					result = allCharacter;
					num = num2;
				}
			}
		}
		return result;
	}

	internal static bool SightBlocked(Vector3 origin, Vector3 direction, float distance, int mask, Transform self, Character target)
	{
		//IL_0028: Unknown result type (might be due to invalid IL or missing references)
		//IL_0029: Unknown result type (might be due to invalid IL or missing references)
		//IL_0032: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: Unknown result type (might be due to invalid IL or missing references)
		//IL_0047: Unknown result type (might be due to invalid IL or missing references)
		//IL_004c: Unknown result type (might be due to invalid IL or missing references)
		if (!Active || (Object)(object)self == (Object)null || !IsDummy(target) || !Eligible(((Component)self).GetComponent<Character>()))
		{
			return Physics.Raycast(origin, direction, distance, mask);
		}
		RaycastHit[] array = Physics.RaycastAll(origin, direction, distance, mask);
		for (int i = 0; i < array.Length; i++)
		{
			RaycastHit val = array[i];
			if ((Object)(object)((RaycastHit)(ref val)).collider == (Object)null || (Object)(object)((Component)((RaycastHit)(ref val)).collider).GetComponentInParent<Character>() != (Object)(object)target)
			{
				return true;
			}
		}
		return false;
	}
}
internal static class TrollXP
{
	[HarmonyPatch(typeof(SpawnAbility), "SetupAoe")]
	private static class CaptureSpawn
	{
		private static void Prefix(SpawnAbility __instance, Character owner, Character ___m_owner)
		{
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
			Player val = (Player)(object)((___m_owner is Player) ? ___m_owner : null);
			if (ready && !((Object)(object)val == (Object)null) && !((Object)(object)val != (Object)(object)Player.m_localPlayer) && ((Character)val).IsOwner() && !((Object)(object)owner == (Object)null) && owner.IsOwner() && TrollXpPolicy.IsSpawn(((Object)((Component)__instance).gameObject).name) && TrollXpPolicy.IsTroll(((Object)((Component)owner).gameObject).name))
			{
				ZDO val2 = Data(owner);
				if (val2 != null)
				{
					val2.Set("local.valheim.bloodmagiccompanion.trollCaster.v1", ((Character)val).GetZDOID());
					CasterXP.Trace(string.Concat("Tracked Trollstav caster ", ((Character)val).GetZDOID(), " for ", owner.GetZDOID()));
				}
			}
		}
	}

	internal sealed class DamageCredit
	{
		internal float Before;

		internal ZDOID Caster;

		internal ZDOID Troll;

		internal ZDOID Victim;
	}

	[HarmonyPatch(typeof(Character), "ApplyDamage")]
	private static class RewardDamage
	{
		private static void Prefix(Character __instance, HitData hit, out DamageCredit __state)
		{
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Invalid comparison between Unknown and I4
			//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
			__state = null;
			if (!ready || hit == null || !__instance.IsOwner() || __instance.IsDead())
			{
				return;
			}
			Character attacker = hit.GetAttacker();
			if ((Object)(object)attacker == (Object)null || !TrollXpPolicy.IsTroll(((Object)((Component)attacker).gameObject).name))
			{
				return;
			}
			ZDO val = Data(attacker);
			if (val != null)
			{
				ZDOID zDOID = val.GetZDOID("local.valheim.bloodmagiccompanion.trollCaster.v1");
				Player val2 = FindCaster(zDOID);
				if (!((Object)(object)val2 == (Object)null) && TrollXpPolicy.Eligible(ready, __instance.IsOwner(), !__instance.IsDead(), __instance.IsPlayer(), __instance.IsTamed(), BaseAI.IsEnemy((Character)(object)val2, __instance), (int)__instance.GetFaction() == 12, (Object)(object)attacker == (Object)(object)__instance, ((Object)((Component)attacker).gameObject).name))
				{
					__state = new DamageCredit
					{
						Before = __instance.GetHealth(),
						Caster = zDOID,
						Troll = attacker.GetZDOID(),
						Victim = __instance.GetZDOID()
					};
				}
			}
		}

		private static void Postfix(Character __instance, DamageCredit __state)
		{
			if (ready && __state != null && TrollXpPolicy.LostHealth(__state.Before, __instance.GetHealth()))
			{
				Report(__state);
			}
		}
	}

	[HarmonyPatch(/*Could not decode attribute arguments.*/)]
	private static class RegisterProtocol
	{
		private static void Postfix(ZRoutedRpc __instance)
		{
			receipts.Clear();
			__instance.Register<ZPackage>("BloodMagicCompanion_TrollHit_v1", (Action<long, ZPackage>)Receive);
		}
	}

	internal const string CasterKey = "local.valheim.bloodmagiccompanion.trollCaster.v1";

	internal const string RpcName = "BloodMagicCompanion_TrollHit_v1";

	internal static readonly int ProtocolHash = StringExtensionMethods.GetStableHashCode("local.valheim.bloodmagiccompanion.trollXp.v1");

	private static readonly ReceiptLedger receipts = new ReceiptLedger(4096);

	private static ConfigEntry<float> factorSetting;

	private static bool ready;

	internal static void Configure(ConfigFile config)
	{
		factorSetting = config.Bind<float>("TrollXP", "Factor", 0.5f, "Blood Magic skill gain factor per accepted damage event (not whole skill levels). Range 0.01-10. Caster setting applies. Restart required.");
	}

	internal static void Patch(Harmony harmony)
	{
		Type[] array = new Type[3]
		{
			typeof(CaptureSpawn),
			typeof(RewardDamage),
			typeof(RegisterProtocol)
		};
		foreach (Type type in array)
		{
			harmony.CreateClassProcessor(type).Patch();
		}
	}

	internal static void SetEnabled(bool enabled)
	{
		ready = enabled;
		receipts.Clear();
		Advertise(Player.m_localPlayer);
	}

	internal static void Advertise(Player player)
	{
		if (!((Object)(object)player == (Object)null))
		{
			ZNetView component = ((Component)player).GetComponent<ZNetView>();
			int num = (ready ? 3 : 0);
			if ((Object)(object)component != (Object)null && component.IsValid() && component.IsOwner() && component.GetZDO().GetInt(ProtocolHash, 0) != num)
			{
				component.GetZDO().Set(ProtocolHash, num, false);
			}
		}
	}

	private static ZDO Data(Character character)
	{
		ZNetView val = (((Object)(object)character == (Object)null) ? null : ((Component)character).GetComponent<ZNetView>());
		if (!((Object)(object)val != (Object)null) || !val.IsValid())
		{
			return null;
		}
		return val.GetZDO();
	}

	private static ZDO Lookup(ZDOID id)
	{
		//IL_000c: Unknown result type (might be due to invalid IL or missing references)
		if (ZDOMan.instance != null)
		{
			return ZDOMan.instance.GetZDO(id);
		}
		return null;
	}

	private static Player FindCaster(ZDOID id)
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		GameObject val = (((Object)(object)ZNetScene.instance == (Object)null) ? null : ZNetScene.instance.FindInstance(id));
		if (!((Object)(object)val == (Object)null))
		{
			return val.GetComponent<Player>();
		}
		return null;
	}

	private static void Report(DamageCredit credit)
	{
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0018: Unknown result type (might be due to invalid IL or missing references)
		//IL_001e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_008d: Expected O, but got Unknown
		//IL_0090: Unknown result type (might be due to invalid IL or missing references)
		//IL_009d: Unknown result type (might be due to invalid IL or missing references)
		//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
		Player localPlayer = Player.m_localPlayer;
		bool casterIsLocal = (Object)(object)localPlayer != (Object)null && ((Character)localPlayer).IsOwner() && ((Character)localPlayer).GetZDOID() == credit.Caster;
		ZDO val = Lookup(credit.Caster);
		long num = ((val == null) ? 0 : val.GetOwner());
		switch (RewardRouting.Select(ownsRecipient: true, consumed: false, casterIsLocal, (val != null) ? val.GetInt(ProtocolHash, 0) : 0, num, ZRoutedRpc.instance != null))
		{
		case RewardRoute.LocalCaster:
			Award(localPlayer, credit.Troll);
			break;
		case RewardRoute.RemoteCaster:
		{
			ZPackage val2 = new ZPackage();
			val2.Write(credit.Caster);
			val2.Write(credit.Troll);
			val2.Write(credit.Victim);
			val2.Write(Guid.NewGuid().ToString("N"));
			ZRoutedRpc.instance.InvokeRoutedRPC(num, "BloodMagicCompanion_TrollHit_v1", new object[1] { val2 });
			break;
		}
		}
	}

	private static void Award(Player player, ZDOID troll)
	{
		//IL_0081: Unknown result type (might be due to invalid IL or missing references)
		if (ready && !((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer) && ((Character)player).IsOwner())
		{
			Skills skills = ((Character)player).GetSkills();
			if (!((Object)(object)skills == (Object)null))
			{
				float num = CostModel.Setting(factorSetting.Value, 0.01f, 10f, 0.5f);
				skills.RaiseSkill((SkillType)10, num);
				CasterXP.Trace("Awarded Trollstav Blood Magic XP to local caster, factor " + num + ", troll " + troll);
			}
		}
	}

	private static void Receive(long sender, ZPackage packet)
	{
		//IL_0025: Unknown result type (might be due to invalid IL or missing references)
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0031: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: Unknown result type (might be due to invalid IL or missing references)
		//IL_0038: Unknown result type (might be due to invalid IL or missing references)
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0050: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_0059: Unknown result type (might be due to invalid IL or missing references)
		//IL_0066: Unknown result type (might be due to invalid IL or missing references)
		//IL_006e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0081: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
		if (!ready)
		{
			return;
		}
		try
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || !((Character)localPlayer).IsOwner())
			{
				return;
			}
			ZDOID val = packet.ReadZDOID();
			ZDOID val2 = packet.ReadZDOID();
			ZDOID val3 = packet.ReadZDOID();
			string token = packet.ReadString();
			if (!(val != ((Character)localPlayer).GetZDOID()) && !(val3 == val) && !(val3 == val2))
			{
				ZDO val4 = Lookup(val3);
				ZDO val5 = Lookup(val2);
				if (val5 != null && !(val5.GetZDOID("local.valheim.bloodmagiccompanion.trollCaster.v1") != val) && val5.GetPrefab() == StringExtensionMethods.GetStableHashCode("Troll_Summoned") && ReceiptLedger.IsAuthorizedSender(sender, (val4 == null) ? ((long?)null) : new long?(val4.GetOwner())) && receipts.Accept(sender, token, 1f))
				{
					Award(localPlayer, val2);
				}
			}
		}
		catch (Exception ex)
		{
			CasterXP.Trace("Ignored invalid Trollstav XP report: " + ex.Message);
		}
	}
}
internal static class TrollXpPolicy
{
	internal static bool IsTroll(string name)
	{
		if (!(name == "Troll_Summoned"))
		{
			return name == "Troll_Summoned(Clone)";
		}
		return true;
	}

	internal static bool IsSpawn(string name)
	{
		if (!(name == "staff_redtroll_spawn"))
		{
			return name == "staff_redtroll_spawn(Clone)";
		}
		return true;
	}

	internal static bool Eligible(bool active, bool ownsVictim, bool living, bool player, bool tame, bool enemy, bool dummy, bool self, string attacker)
	{
		if (active && ownsVictim && living && !player && !tame && enemy && !dummy && !self)
		{
			return IsTroll(attacker);
		}
		return false;
	}

	internal static bool LostHealth(float before, float after)
	{
		if (CostModel.Finite(before) && CostModel.Finite(after) && before > 0f)
		{
			return after < before;
		}
		return false;
	}
}