using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: AssemblyFileVersion("1.1.1.0")] [assembly: CompilationRelaxations(8)] [assembly: AssemblyProduct("HarnessForceJade")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyTitle("HarnessForceJade")] [assembly: AssemblyVersion("1.1.1.0")] namespace HarnessForceJade; public static class PluginInfo { public const string PLUGIN_GUID = "supidowagon.HarnessForceJade"; public const string PLUGIN_NAME = "Harness Force Jade"; public const string PLUGIN_VERSION = "1.1.1"; } [BepInPlugin("supidowagon.HarnessForceJade", "Harness Force Jade", "1.1.1")] public class Plugin : BaseUnityPlugin { [HarmonyPatch] private static class Patch_PlayerParryState_AccurateParry { private static IEnumerable TargetMethods() { EnsureLookups(); Type stateType = AccessTools.TypeByName("PlayerParryState"); if (stateType == null) { Logger.LogError((object)"Could not find PlayerParryState; the jade keeps its stock behaviour."); yield break; } MethodBase target = AccessTools.Method(stateType, "AccurateParry", (Type[])null, (Type[])null); if (target == null) { Logger.LogError((object)"Could not find PlayerParryState.AccurateParry; the jade keeps its stock behaviour."); } else { yield return target; } } [HarmonyPrefix] private static void Prefix() { GrantFullCharge(); } } [HarmonyPatch] private static class Patch_Player_isReleasingChargeAttack { private static IEnumerable TargetMethods() { Type playerType = AccessTools.TypeByName("Player"); if (!(playerType == null)) { MethodBase target = AccessTools.Method(playerType, "get_isReleasingChargeAttack", (Type[])null, (Type[])null); if (target == null) { Logger.LogError((object)"Could not find Player.isReleasingChargeAttack; a banked charge would fire on its own straight out of the parry. Disable this mod."); } else { yield return target; } } } [HarmonyPrefix] private static bool Prefix(object __instance, ref bool __result) { return HoldBankedCharge(__instance, ref __result); } } private const float FullChargeTimerBump = 999f; internal static ManualLogSource Logger; internal static ConfigEntry Enabled; internal static ConfigEntry RequireJadeEquipped; internal static ConfigEntry FireOnAttackPress; internal static ConfigEntry BankSeconds; internal static ConfigEntry LogChargeBank; private static bool banked; private static float bankedAt; private static bool lookupTried; private static MethodInfo playerInstanceGetter; private static FieldInfo isChargedField; private static FieldInfo chargedTimerField; private static MethodInfo playChargedEffect; private static MethodInfo chargingReleasedOrCanceled; private static PropertyInfo mainAbilitiesProp; private static FieldInfo jadeField; private static PropertyInfo abilityIsActivatedProp; private static PropertyInfo actionsProp; private static FieldInfo attackActionField; private static PropertyInfo isPressedProp; private static PropertyInfo wasPressedProp; private static bool inputLookupTried; private void Awake() { Logger = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Turn off to restore the jade's stock behaviour without removing the mod."); RequireJadeEquipped = ((BaseUnityPlugin)this).Config.Bind("General", "RequireJadeEquipped", true, "Only bank a charge when the jade is actually equipped, so unequipping it gives you vanilla. Turn off to get the effect for free, without spending the slot."); FireOnAttackPress = ((BaseUnityPlugin)this).Config.Bind("General", "FireOnAttackPress", true, "When on, the banked charge comes out the moment you press attack. Turn off and pressing attack does a normal swing instead, with the charged attack coming out when you release - the way charging normally works."); BankSeconds = ((BaseUnityPlugin)this).Config.Bind("General", "BankSeconds", 0f, "How long a banked charge waits for you, in seconds. 0 or less means it waits until you use it. When it does expire the charge is dropped rather than fired."); LogChargeBank = ((BaseUnityPlugin)this).Config.Bind("Diagnostics", "LogChargeBank", false, "Log every charge banked and released. Off by default because it is noisy; turn it on only when investigating."); Logger.LogInfo((object)"Plugin supidowagon.HarnessForceJade is loaded!"); Harmony val = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly(), "supidowagon.HarnessForceJade"); foreach (MethodBase patchedMethod in val.GetPatchedMethods()) { Logger.LogInfo((object)("Patched method: " + patchedMethod.DeclaringType.FullName + "." + patchedMethod.Name)); } if (val.GetPatchedMethods().Count() == 0) { Logger.LogError((object)"Failed to apply Harmony patches."); } } private static MethodInfo FindMethod(Type type, string name) { Type type2 = type; while (type2 != null) { try { MethodInfo method = type2.GetMethod(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (method != null) { return method; } } catch (AmbiguousMatchException) { return null; } type2 = type2.BaseType; } return null; } private static FieldInfo FindField(Type type, string name) { Type type2 = type; while (type2 != null) { FieldInfo field = type2.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field; } type2 = type2.BaseType; } return null; } private static PropertyInfo FindProperty(Type type, string name) { Type type2 = type; while (type2 != null) { try { PropertyInfo property = type2.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { return property; } } catch (AmbiguousMatchException) { return null; } type2 = type2.BaseType; } return null; } private static void EnsureLookups() { if (lookupTried) { return; } lookupTried = true; Type type = AccessTools.TypeByName("Player"); if (type == null) { Logger.LogError((object)"Could not find the Player type; the jade keeps its stock behaviour."); return; } playerInstanceGetter = FindMethod(type, "get_i"); isChargedField = FindField(type, "isCharged"); chargedTimerField = FindField(type, "attackChargedTimer"); playChargedEffect = FindMethod(type, "PlayChargedEffect"); chargingReleasedOrCanceled = FindMethod(type, "ChargingReleasedOrCanceled"); mainAbilitiesProp = FindProperty(type, "mainAbilities"); actionsProp = FindProperty(type, "actions"); Type type2 = AccessTools.TypeByName("PlayerMainAbilityCollection"); if (type2 != null) { jadeField = FindField(type2, "AttackChargedWhenParryJade"); } Type type3 = AccessTools.TypeByName("PlayerAbilityData"); if (type3 != null) { abilityIsActivatedProp = FindProperty(type3, "IsActivated"); } Type type4 = AccessTools.TypeByName("PlayerGamePlayActionSet"); if (type4 != null) { attackActionField = FindField(type4, "Attack"); } List list = new List(); if (playerInstanceGetter == null) { list.Add("Player.i"); } if (isChargedField == null) { list.Add("Player.isCharged"); } if (chargedTimerField == null) { list.Add("Player.attackChargedTimer"); } if (actionsProp == null) { list.Add("Player.actions"); } if (attackActionField == null) { list.Add("PlayerGamePlayActionSet.Attack"); } if (mainAbilitiesProp == null) { list.Add("Player.mainAbilities"); } if (jadeField == null) { list.Add("PlayerMainAbilityCollection.AttackChargedWhenParryJade"); } if (abilityIsActivatedProp == null) { list.Add("PlayerAbilityData.IsActivated"); } if (list.Count > 0) { Logger.LogError((object)("Could not resolve " + string.Join(", ", list.ToArray()) + "; the jade keeps its stock behaviour. This usually means a game update renamed something - disable this mod until it is rebuilt.")); } if (playChargedEffect == null) { Logger.LogWarning((object)"Could not find Player.PlayChargedEffect; a banked charge will work but will not show the charged glow."); } if (chargingReleasedOrCanceled == null) { Logger.LogWarning((object)"Could not find Player.ChargingReleasedOrCanceled; BankSeconds cannot drop an expired charge and will be ignored."); } } private static object PlayerInstance() { EnsureLookups(); if (playerInstanceGetter == null) { return null; } try { return playerInstanceGetter.Invoke(null, null); } catch { return null; } } private static object AttackAction(object player) { if (player == null || actionsProp == null || attackActionField == null) { return null; } try { object value = actionsProp.GetValue(player, null); return (value == null) ? null : attackActionField.GetValue(value); } catch { return null; } } private static bool ReadInputFlag(object player, bool wasPressed) { object obj = AttackAction(player); if (obj == null) { return false; } if (!inputLookupTried) { inputLookupTried = true; isPressedProp = FindProperty(obj.GetType(), "IsPressed"); wasPressedProp = FindProperty(obj.GetType(), "WasPressed"); if (isPressedProp == null) { Logger.LogError((object)("Could not find IsPressed on " + obj.GetType().FullName + "; the jade keeps its stock behaviour.")); } } PropertyInfo propertyInfo = (wasPressed ? wasPressedProp : isPressedProp); if (propertyInfo == null) { return false; } try { return (bool)propertyInfo.GetValue(obj, null); } catch { return false; } } private static bool AttackHeld(object player) { return ReadInputFlag(player, wasPressed: false); } private static bool AttackJustPressed(object player) { return ReadInputFlag(player, wasPressed: true); } private static bool JadeEquipped(object player) { if (player == null || mainAbilitiesProp == null || jadeField == null || abilityIsActivatedProp == null) { return false; } try { object value = mainAbilitiesProp.GetValue(player, null); if (value == null) { return false; } object value2 = jadeField.GetValue(value); if (value2 == null) { return false; } return (bool)abilityIsActivatedProp.GetValue(value2, null); } catch { return false; } } private static bool IsCharged(object player) { if (player == null || isChargedField == null) { return false; } try { return (bool)isChargedField.GetValue(player); } catch { return false; } } private static void CancelCharge(object player) { if (chargingReleasedOrCanceled == null) { return; } try { MethodInfo methodInfo = chargingReleasedOrCanceled; object[] parameters = new object[1]; methodInfo.Invoke(player, parameters); } catch (Exception ex) { Logger.LogWarning((object)("Could not drop the expired charge: " + ex.Message)); } } internal static void GrantFullCharge() { if (Enabled != null && !Enabled.Value) { return; } object obj = PlayerInstance(); if (obj == null || isChargedField == null || chargedTimerField == null || ((RequireJadeEquipped == null || RequireJadeEquipped.Value) && !JadeEquipped(obj)) || IsCharged(obj)) { return; } bool flag = AttackHeld(obj); try { isChargedField.SetValue(obj, true); chargedTimerField.SetValue(obj, (float)chargedTimerField.GetValue(obj) + 999f); } catch (Exception ex) { Logger.LogError((object)("Could not grant the charge: " + ex.Message)); return; } if (playChargedEffect != null) { try { MethodInfo methodInfo = playChargedEffect; object[] parameters = new object[1]; methodInfo.Invoke(obj, parameters); } catch (Exception ex2) { Logger.LogWarning((object)("Could not play the charged effect: " + ex2.Message)); } } banked = !flag; bankedAt = Time.time; if (LogChargeBank != null && LogChargeBank.Value) { Logger.LogInfo((object)(flag ? "Perfect parry: full charge granted while attack was held (stock path)." : "Perfect parry: full charge banked, waiting for your next attack.")); } } internal static bool HoldBankedCharge(object player, ref bool result) { if (!banked) { return true; } if (Enabled != null && !Enabled.Value) { banked = false; return true; } if (!IsCharged(player)) { banked = false; return true; } float num = ((BankSeconds == null) ? 0f : BankSeconds.Value); if (num > 0f && Time.time - bankedAt > num) { banked = false; CancelCharge(player); if (LogChargeBank != null && LogChargeBank.Value) { Logger.LogInfo((object)("Banked charge expired after " + num + "s and was dropped.")); } result = false; return false; } if (!AttackHeld(player) && !AttackJustPressed(player)) { result = false; return false; } banked = false; if (FireOnAttackPress == null || FireOnAttackPress.Value) { if (LogChargeBank != null && LogChargeBank.Value) { Logger.LogInfo((object)"Banked charge released on attack press."); } result = true; return false; } if (LogChargeBank != null && LogChargeBank.Value) { Logger.LogInfo((object)"Banked charge handed back to the stock release rules."); } return true; } }