Decompiled source of Soulcatcher Crossbow Fix v1.0.1

SoulcatcherCrossbowFix.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
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.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("SoulcatcherCrossbowFix")]
[assembly: AssemblyConfiguration("release")]
[assembly: AssemblyDescription("Fixes Crossbow interactions with Soulcatcher mod")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyInformationalVersion("1.0.1+61aedf875640c084f5973ccd858d0d43f753a701")]
[assembly: AssemblyProduct("SoulcatcherCrossbowFix")]
[assembly: AssemblyTitle("SoulcatcherCrossbowFix")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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 SoulcatcherCrossbowFix
{
	[BepInPlugin("ru.custom.soulcatcher_crossbowfix", "Soulcatcher Crossbow Fix", "1.0.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		[HarmonyPatch(typeof(SEMan), "ModifyAttack")]
		private static class SEMan_ModifyAttack_Patch
		{
			private static void Postfix(SEMan __instance, SkillType skill, ref HitData hitData)
			{
				try
				{
					if (CfgEnableDamage == null || !CfgEnableDamage.Value)
					{
						return;
					}
					object obj = (object)((_fiSEMan_m_character != null) ? /*isinst with value type is only supported in some contexts*/: null);
					Player val = (Player)((obj is Player) ? obj : null);
					if (val == null)
					{
						return;
					}
					ItemData currentWeapon = ((Humanoid)val).GetCurrentWeapon();
					if (!IsCrossbow(currentWeapon))
					{
						return;
					}
					float effectPercent = GetEffectPercent(val, (CfgEffectNameHare != null) ? CfgEffectNameHare.Value : "Hare Soul Power");
					if (effectPercent <= 0f)
					{
						DebugLogRate("[DMG] Hare value=0 (no gem or not applied).");
						return;
					}
					float num = 1f + effectPercent / 100f;
					MultiplyDamage(ref hitData.m_damage, num);
					object arg = effectPercent;
					object arg2 = num;
					object arg3;
					if (currentWeapon == null)
					{
						arg3 = null;
					}
					else
					{
						GameObject dropPrefab = currentWeapon.m_dropPrefab;
						arg3 = ((dropPrefab != null) ? ((Object)dropPrefab).name : null);
					}
					DebugLogRate($"[DMG] Applied Hare. value={arg:0.###}% mult={arg2:0.###} weapon={arg3}");
					DebugDumpWeaponCustomDataRate(currentWeapon);
				}
				catch (Exception arg4)
				{
					LogError($"[DMG] ModifyAttack failed: {arg4}");
				}
			}
		}

		[HarmonyPatch(typeof(ItemData), "GetWeaponLoadingTime")]
		private static class ItemData_GetWeaponLoadingTime_Patch
		{
			private static void Postfix(ItemData __instance, ref float __result)
			{
				try
				{
					if (CfgEnableReload == null || !CfgEnableReload.Value)
					{
						return;
					}
					Player localPlayer = Player.m_localPlayer;
					if ((Object)(object)localPlayer == (Object)null || __instance != ((Humanoid)localPlayer).GetCurrentWeapon() || !IsCrossbow(__instance))
					{
						return;
					}
					float effectPercent = GetEffectPercent(localPlayer, (CfgEffectNameDverger != null) ? CfgEffectNameDverger.Value : "Dverger Soul Power");
					if (effectPercent <= 0f)
					{
						DebugLogRate("[RLD] Dverger value=0 (no gem or not applied).");
						return;
					}
					float num = Mathf.Clamp01(1f - effectPercent / 100f);
					float num2 = __result;
					__result *= num;
					float num3 = Mathf.Max(0f, (CfgReloadMinTime != null) ? CfgReloadMinTime.Value : 0f);
					if (num3 > 0f)
					{
						__result = Mathf.Max(__result, num3);
					}
					object[] obj = new object[5] { effectPercent, num, num2, __result, null };
					object obj2;
					if (__instance == null)
					{
						obj2 = null;
					}
					else
					{
						GameObject dropPrefab = __instance.m_dropPrefab;
						obj2 = ((dropPrefab != null) ? ((Object)dropPrefab).name : null);
					}
					obj[4] = obj2;
					DebugLogRate(string.Format("[RLD] Applied Dverger. value={0:0.###}% timeMult={1:0.###} loading {2:0.###}->{3:0.###} weapon={4}", obj));
					DebugDumpWeaponCustomDataRate(__instance);
				}
				catch (Exception arg)
				{
					LogError($"[RLD] GetWeaponLoadingTime failed: {arg}");
				}
			}
		}

		[HarmonyPatch(typeof(StatusEffect), "GetTooltipString")]
		private static class StatusEffect_GetTooltipString_Patch
		{
			private static void Postfix(StatusEffect __instance, ref string __result)
			{
				try
				{
					if (string.IsNullOrEmpty(__result) || __result.IndexOf("crossbow", StringComparison.OrdinalIgnoreCase) < 0)
					{
						return;
					}
					Player localPlayer = Player.m_localPlayer;
					if ((Object)(object)localPlayer == (Object)null)
					{
						return;
					}
					string effectName = ((CfgEffectNameHare != null) ? CfgEffectNameHare.Value : "Hare Soul Power");
					string effectName2 = ((CfgEffectNameDverger != null) ? CfgEffectNameDverger.Value : "Dverger Soul Power");
					float effectPercent = GetEffectPercent(localPlayer, effectName);
					float effectPercent2 = GetEffectPercent(localPlayer, effectName2);
					if (!(effectPercent <= 0f) || !(effectPercent2 <= 0f))
					{
						string text = InjectNumbers(__result, effectPercent, effectPercent2);
						if (!string.Equals(text, __result, StringComparison.Ordinal))
						{
							__result = text;
						}
					}
				}
				catch (Exception arg)
				{
					LogError($"[UI] Active cells tooltip patch failed: {arg}");
				}
			}

			private static string InjectNumbers(string tooltip, float harePct, float dverPct)
			{
				string[] array = tooltip.Split(new char[1] { '\n' });
				bool flag = false;
				for (int i = 0; i < array.Length; i++)
				{
					string text = array[i];
					string text2 = text.Trim();
					if (dverPct > 0f && ContainsAll(text2, "crossbow", "reload"))
					{
						array[i] = ReplaceKeepIndent(text, $"Crossbow reload time reduced by {dverPct:0.##}%.");
						flag = true;
					}
					else if (harePct > 0f && ContainsAll(text2, "crossbow", "damage") && text2.IndexOf("reload", StringComparison.OrdinalIgnoreCase) < 0)
					{
						array[i] = ReplaceKeepIndent(text, $"Crossbow damage increased by {harePct:0.##}%.");
						flag = true;
					}
				}
				if (!flag)
				{
					return tooltip;
				}
				return string.Join("\n", array);
			}

			private static bool ContainsAll(string s, params string[] parts)
			{
				if (string.IsNullOrEmpty(s) || parts == null || parts.Length == 0)
				{
					return false;
				}
				for (int i = 0; i < parts.Length; i++)
				{
					if (s.IndexOf(parts[i], StringComparison.OrdinalIgnoreCase) < 0)
					{
						return false;
					}
				}
				return true;
			}

			private static string ReplaceKeepIndent(string originalLine, string newText)
			{
				if (string.IsNullOrEmpty(originalLine))
				{
					return newText;
				}
				int i;
				for (i = 0; i < originalLine.Length && char.IsWhiteSpace(originalLine[i]); i++)
				{
				}
				if (i <= 0)
				{
					return newText;
				}
				return originalLine.Substring(0, i) + newText;
			}
		}

		private static ManualLogSource Log;

		private Harmony _harmony;

		private static ConfigEntry<bool> CfgEnableDamage;

		private static ConfigEntry<bool> CfgEnableReload;

		private static ConfigEntry<float> CfgReloadMinTime;

		private static ConfigEntry<bool> CfgDebug;

		private static ConfigEntry<bool> CfgDebugDumpWeaponCustomData;

		private static ConfigEntry<bool> CfgDebugDumpDiscovery;

		private static ConfigEntry<float> CfgDebugMinIntervalSec;

		private static ConfigEntry<string> CfgEffectNameHare;

		private static ConfigEntry<string> CfgEffectNameDverger;

		private static float _nextLogTime;

		private const string DefaultEffectHare = "Hare Soul Power";

		private const string DefaultEffectDverger = "Dverger Soul Power";

		private static MethodInfo _miGetEffectPowerGenericDef;

		private static Type _cfgTypeHare;

		private static Type _cfgTypeDverger;

		private static float _nextResolveAttemptTime;

		private static FieldInfo _fiSEMan_m_character;

		private void Awake()
		{
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_016a: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			CfgEnableDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableHareDamageBonus", true, "Enable Hare gem damage bonus for crossbows.");
			CfgEnableReload = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableDvergerReloadBonus", true, "Enable Dverger gem reload time reduction for crossbows.");
			CfgReloadMinTime = ((BaseUnityPlugin)this).Config.Bind<float>("General", "MinReloadTime", 0.3f, "Minimum reload time for crossbows when the Dverger gem effect reduces it.");
			CfgEffectNameHare = ((BaseUnityPlugin)this).Config.Bind<string>("Effects", "HareEffectName", "Hare Soul Power", "Jewelcrafting effect name for Hare gem.");
			CfgEffectNameDverger = ((BaseUnityPlugin)this).Config.Bind<string>("Effects", "DvergerEffectName", "Dverger Soul Power", "Jewelcrafting effect name for Dverger gem.");
			CfgDebug = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DebugEnabled", false, "Enable verbose debug logging.");
			CfgDebugDumpWeaponCustomData = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DumpWeaponCustomData", false, "Dump current weapon m_customData (spammy).");
			CfgDebugDumpDiscovery = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "DumpDiscovery", true, "Log discovery of GetEffectPower method and config types.");
			CfgDebugMinIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Debug", "MinLogIntervalSeconds", 0.5f, "Rate limit for repeated debug logs.");
			_fiSEMan_m_character = AccessTools.Field(typeof(SEMan), "m_character");
			ResolveJewelcraftingBindings(force: true);
			_harmony = new Harmony("ru.custom.soulcatcher_crossbowfix");
			_harmony.PatchAll();
			LogInfo("[Init] Loaded v1.0.1");
		}

		private static bool IsCrossbow(ItemData weapon)
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001a: Invalid comparison between Unknown and I4
			if (weapon == null || weapon.m_shared == null)
			{
				return false;
			}
			if ((int)weapon.m_shared.m_skillType == 14)
			{
				return true;
			}
			string text = (Object.op_Implicit((Object)(object)weapon.m_dropPrefab) ? ((Object)weapon.m_dropPrefab).name : null);
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			return text.StartsWith("Crossbow", StringComparison.OrdinalIgnoreCase);
		}

		private static void MultiplyDamage(ref DamageTypes dmg, float mult)
		{
			dmg.m_damage *= mult;
			dmg.m_pierce *= mult;
			dmg.m_blunt *= mult;
			dmg.m_slash *= mult;
			dmg.m_fire *= mult;
			dmg.m_frost *= mult;
			dmg.m_lightning *= mult;
			dmg.m_poison *= mult;
			dmg.m_spirit *= mult;
			dmg.m_chop *= mult;
			dmg.m_pickaxe *= mult;
		}

		private static float GetEffectPercent(Player player, string effectName)
		{
			if ((Object)(object)player == (Object)null)
			{
				return 0f;
			}
			if (string.IsNullOrWhiteSpace(effectName))
			{
				return 0f;
			}
			ResolveJewelcraftingBindings(force: false);
			if (_miGetEffectPowerGenericDef == null)
			{
				DebugLogRate("[JC] GetEffectPower<T> not resolved.");
				return 0f;
			}
			Type type = null;
			if (CfgEffectNameHare != null && string.Equals(effectName, CfgEffectNameHare.Value, StringComparison.OrdinalIgnoreCase))
			{
				type = _cfgTypeHare;
			}
			else if (CfgEffectNameDverger != null && string.Equals(effectName, CfgEffectNameDverger.Value, StringComparison.OrdinalIgnoreCase))
			{
				type = _cfgTypeDverger;
			}
			if (type == null)
			{
				if (effectName.IndexOf("Hare", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					type = _cfgTypeHare;
				}
				if (effectName.IndexOf("Dver", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					type = _cfgTypeDverger;
				}
			}
			if (type == null)
			{
				DebugLogRate("[JC] Config type not resolved for effect '" + effectName + "'.");
				return 0f;
			}
			try
			{
				object obj = _miGetEffectPowerGenericDef.MakeGenericMethod(type).Invoke(null, new object[2] { player, effectName });
				if (obj == null)
				{
					return 0f;
				}
				float num = ExtractFirstFloat(type, obj);
				if (CfgDebug != null && CfgDebug.Value && CfgDebugDumpDiscovery != null && CfgDebugDumpDiscovery.Value)
				{
					LogInfo($"[JC] Effect '{effectName}' cfgType='{type.FullName}' value={num:0.###}");
				}
				return num;
			}
			catch (Exception ex)
			{
				DebugLogRate("[JC] GetEffectPower invoke failed: " + ex.GetType().Name + ": " + ex.Message);
				return 0f;
			}
		}

		private static float ExtractFirstFloat(Type cfgType, object cfgObj)
		{
			try
			{
				FieldInfo field = cfgType.GetField("Value", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field != null && field.FieldType == typeof(float))
				{
					return (float)field.GetValue(cfgObj);
				}
				FieldInfo field2 = cfgType.GetField("Power", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (field2 != null && field2.FieldType == typeof(float))
				{
					return (float)field2.GetValue(cfgObj);
				}
				FieldInfo fieldInfo = cfgType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((FieldInfo x) => x.FieldType == typeof(float));
				if (fieldInfo != null)
				{
					return (float)fieldInfo.GetValue(cfgObj);
				}
				PropertyInfo propertyInfo = cfgType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((PropertyInfo x) => x.PropertyType == typeof(float) && x.GetIndexParameters().Length == 0);
				if (propertyInfo != null)
				{
					return (float)propertyInfo.GetValue(cfgObj, null);
				}
			}
			catch
			{
			}
			return 0f;
		}

		private static void ResolveJewelcraftingBindings(bool force)
		{
			if (!force)
			{
				if (Time.time < _nextResolveAttemptTime)
				{
					return;
				}
				_nextResolveAttemptTime = Time.time + 1f;
			}
			bool num = _miGetEffectPowerGenericDef != null;
			bool flag = _cfgTypeHare != null && _cfgTypeDverger != null;
			if (num && flag)
			{
				return;
			}
			try
			{
				if (_miGetEffectPowerGenericDef == null)
				{
					Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
					foreach (Assembly assembly in assemblies)
					{
						Type[] types;
						try
						{
							types = assembly.GetTypes();
						}
						catch
						{
							continue;
						}
						foreach (Type type in types)
						{
							if (type == null)
							{
								continue;
							}
							MethodInfo[] methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
							foreach (MethodInfo methodInfo in methods)
							{
								if (!(methodInfo == null) && !(methodInfo.Name != "GetEffectPower") && methodInfo.IsGenericMethodDefinition)
								{
									ParameterInfo[] parameters = methodInfo.GetParameters();
									if (parameters.Length == 2 && !(parameters[0].ParameterType != typeof(Player)) && !(parameters[1].ParameterType != typeof(string)))
									{
										_miGetEffectPowerGenericDef = methodInfo;
										break;
									}
								}
							}
							if (_miGetEffectPowerGenericDef != null)
							{
								break;
							}
						}
						if (_miGetEffectPowerGenericDef != null)
						{
							break;
						}
					}
					if (CfgDebug != null && CfgDebug.Value && CfgDebugDumpDiscovery != null && CfgDebugDumpDiscovery.Value)
					{
						LogInfo((_miGetEffectPowerGenericDef != null) ? ("[DISCOVERY] Found GetEffectPower<T>: " + _miGetEffectPowerGenericDef.DeclaringType?.FullName + "::" + _miGetEffectPowerGenericDef.Name) : "[DISCOVERY] GetEffectPower<T> not found (yet).");
					}
				}
			}
			catch (Exception arg)
			{
				if (CfgDebug != null && CfgDebug.Value)
				{
					LogError($"[DISCOVERY] GetEffectPower<T> search failed: {arg}");
				}
			}
			try
			{
				if (_cfgTypeHare == null)
				{
					_cfgTypeHare = FindTypeByFragments(new string[3] { "Hare", "Soul", "Power" });
				}
				if (_cfgTypeDverger == null)
				{
					_cfgTypeDverger = FindTypeByFragments(new string[3] { "Dver", "Soul", "Power" });
				}
				if (CfgDebug != null && CfgDebug.Value && CfgDebugDumpDiscovery != null && CfgDebugDumpDiscovery.Value)
				{
					LogInfo("[DISCOVERY] Hare cfgType: " + (_cfgTypeHare?.FullName ?? "NOT FOUND"));
					LogInfo("[DISCOVERY] Dverger cfgType: " + (_cfgTypeDverger?.FullName ?? "NOT FOUND"));
				}
			}
			catch (Exception arg2)
			{
				if (CfgDebug != null && CfgDebug.Value)
				{
					LogError($"[DISCOVERY] Config type search failed: {arg2}");
				}
			}
		}

		private static Type FindTypeByFragments(string[] fragments)
		{
			foreach (Assembly item in AppDomain.CurrentDomain.GetAssemblies().OrderByDescending(delegate(Assembly a)
			{
				string? obj2 = a.GetName().Name ?? "";
				int num = 0;
				if (obj2.IndexOf("Soulcatcher", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					num += 2;
				}
				if (obj2.IndexOf("Jewelcrafting", StringComparison.OrdinalIgnoreCase) >= 0)
				{
					num++;
				}
				return num;
			}))
			{
				Type[] types;
				try
				{
					types = item.GetTypes();
				}
				catch
				{
					continue;
				}
				Type[] array = types;
				foreach (Type type in array)
				{
					if (type == null)
					{
						continue;
					}
					string fullName = type.FullName;
					if (string.IsNullOrEmpty(fullName))
					{
						continue;
					}
					bool flag = true;
					for (int j = 0; j < fragments.Length; j++)
					{
						if (fullName.IndexOf(fragments[j], StringComparison.OrdinalIgnoreCase) < 0)
						{
							flag = false;
							break;
						}
					}
					if (flag && (type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any((FieldInfo f) => f.FieldType == typeof(float)) || type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Any((PropertyInfo p) => p.PropertyType == typeof(float) && p.GetIndexParameters().Length == 0)))
					{
						return type;
					}
				}
			}
			return null;
		}

		private static void LogInfo(string msg)
		{
			try
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogInfo((object)msg);
				}
			}
			catch
			{
				Debug.Log((object)msg);
			}
		}

		private static void LogError(string msg)
		{
			try
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogError((object)msg);
				}
			}
			catch
			{
				Debug.LogError((object)msg);
			}
		}

		private static void DebugLogRate(string msg)
		{
			if (CfgDebug != null && CfgDebug.Value)
			{
				float num = ((CfgDebugMinIntervalSec != null) ? Mathf.Max(0.05f, CfgDebugMinIntervalSec.Value) : 0.5f);
				if (!(Time.time < _nextLogTime))
				{
					_nextLogTime = Time.time + num;
					LogInfo(msg);
				}
			}
		}

		private static void DebugDumpWeaponCustomDataRate(ItemData weapon)
		{
			if (CfgDebug == null || !CfgDebug.Value || CfgDebugDumpWeaponCustomData == null || !CfgDebugDumpWeaponCustomData.Value || weapon == null)
			{
				return;
			}
			if (CfgDebugMinIntervalSec != null)
			{
				Mathf.Max(0.05f, CfgDebugMinIntervalSec.Value);
			}
			if (Time.time < _nextLogTime)
			{
				return;
			}
			if (weapon.m_customData == null || weapon.m_customData.Count == 0)
			{
				LogInfo("[DBG] weapon.m_customData is empty");
				return;
			}
			foreach (KeyValuePair<string, string> customDatum in weapon.m_customData)
			{
				string text = customDatum.Value ?? "";
				if (text.Length > 160)
				{
					text = text.Substring(0, 160) + "...";
				}
				LogInfo("[DBG] customData: '" + customDatum.Key + "' = '" + text + "'");
			}
		}
	}
}