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<MethodBase> 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<MethodBase> 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<bool> Enabled;
internal static ConfigEntry<bool> RequireJadeEquipped;
internal static ConfigEntry<bool> FireOnAttackPress;
internal static ConfigEntry<float> BankSeconds;
internal static ConfigEntry<bool> 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<bool>("General", "Enabled", true, "Turn off to restore the jade's stock behaviour without removing the mod.");
RequireJadeEquipped = ((BaseUnityPlugin)this).Config.Bind<bool>("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<bool>("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<float>("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<bool>("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<string> list = new List<string>();
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;
}
}