From af37d4ab426886337a94dee2b42cfdc1c9dcc031 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Wed, 3 Mar 2021 21:54:06 -0500 Subject: [PATCH] Add fix for cooldown only applying to current held item --- CLre/API/Utility/Logging.cs | 2 +- CLre/API/Utility/Reflection.cs | 115 +++++++++++++++++ CLre/CLre.cs | 8 ++ CLre/CLre.csproj | 5 +- CLre/Fixes/CooldownCrossSlotSync.cs | 184 ++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 CLre/API/Utility/Reflection.cs create mode 100644 CLre/Fixes/CooldownCrossSlotSync.cs diff --git a/CLre/API/Utility/Logging.cs b/CLre/API/Utility/Logging.cs index 8366b18..2c5d30f 100644 --- a/CLre/API/Utility/Logging.cs +++ b/CLre/API/Utility/Logging.cs @@ -79,7 +79,7 @@ namespace CLre.API.Utility /// /// The object to log [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void MetaDebugLog(object obj) + internal static void MetaDebugLog(object obj) { #if DEBUG MetaLog($"[MetaDebug]{obj.ToString()}"); diff --git a/CLre/API/Utility/Reflection.cs b/CLre/API/Utility/Reflection.cs new file mode 100644 index 0000000..edcc6af --- /dev/null +++ b/CLre/API/Utility/Reflection.cs @@ -0,0 +1,115 @@ +using System; +using System.Reflection; +using HarmonyLib; +using Svelto.DataStructures; +using Svelto.ECS; + +namespace CLre.API.Utility +{ + public static class Reflection + { + // useful function & method prototypes + public delegate T Getter(); + + public delegate bool ExistsV1(EGID egid); + + public delegate bool ExistsV2(int id, int groupid); + + public delegate T QueryEntityViewV1(EGID egid); + + public delegate T QueryEntityViewV2(int id, ExclusiveGroup.ExclusiveGroupStruct groupid); + + public delegate ReadOnlyCollectionStruct QueryEntityViews(int group) where T : class, IEntityViewStruct; + + public delegate T[] QueryEntitiesV2(int group, out int count) where T : IEntityStruct; + + public delegate T[] QueryEntitiesV1(ExclusiveGroup.ExclusiveGroupStruct group, out int count) where T : IEntityStruct; + + // useful reflection functions + public static TFuncProto BuildDelegate(MethodInfo method) where TFuncProto : Delegate + { + return (TFuncProto) Delegate.CreateDelegate(typeof(TFuncProto), method); + } + + public static Delegate BuildDelegateRaw(MethodInfo method, Type TFuncProto) + { + return Delegate.CreateDelegate(TFuncProto, method); + } + + public static TFuncProto BuildDelegate(MethodInfo method, object instance) where TFuncProto : Delegate + { + return (TFuncProto) Delegate.CreateDelegate(typeof(TFuncProto), instance, method, true); + } + + public static Delegate BuildDelegateRaw(MethodInfo method, object instance, Type TFuncProto) + { + return Delegate.CreateDelegate(TFuncProto, instance, method, true); + } + + public static TFuncProto MethodAsDelegate(Type class_, string methodName, Type[] parameters = null, Type[] generics = null, object instance = null) where TFuncProto : Delegate + { + MethodInfo method = AccessTools.Method(class_, methodName, parameters, generics); + if (instance != null) + { + return BuildDelegate(method, instance); + } + return BuildDelegate(method); + } + + public static Delegate MethodAsDelegateRaw(Type class_, Type TFuncProto, string methodName, Type[] parameters = null, Type[] generics = null, object instance = null) + { + MethodInfo method = AccessTools.Method(class_, methodName, parameters, generics); + if (instance != null) + { + return BuildDelegateRaw(method, instance, TFuncProto); + } + return BuildDelegateRaw(method, TFuncProto); + } + + public static TFuncProto MethodAsDelegate(string typeColonName, Type[] parameters = null, Type[] generics = null, object instance = null) where TFuncProto : Delegate + { + MethodInfo method = AccessTools.Method(typeColonName, parameters, generics); + if (instance != null) + { + return BuildDelegate(method, instance); + } + return BuildDelegate(method); + } + + public static Delegate MethodAsDelegateRaw(string typeColonName, Type TFuncProto, Type[] parameters = null, Type[] generics = null, object instance = null) + { + MethodInfo method = AccessTools.Method(typeColonName, parameters, generics); + if (instance != null) + { + return BuildDelegateRaw(method, instance, TFuncProto); + } + return BuildDelegateRaw(method, TFuncProto); + } + + public static PropertyInfo GetIndexer(this Type type, Type[] arguments = null, BindingFlags bindingFlags = BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.SetField | BindingFlags.SetProperty | BindingFlags.Static) + { + foreach (PropertyInfo p in type.GetProperties(bindingFlags)) + { + if (arguments == null && p.GetIndexParameters().Length != 0) return p; + if (arguments != null) + { + uint count = 0; + foreach (ParameterInfo param in p.GetIndexParameters()) + { + if (param.ParameterType != arguments[count]) + { + break; + } + + count++; + } + if (count == arguments.Length) + { + return p; + } + } + } + return null; + } + } +} \ No newline at end of file diff --git a/CLre/CLre.cs b/CLre/CLre.cs index 009a236..86e80e3 100644 --- a/CLre/CLre.cs +++ b/CLre/CLre.cs @@ -83,5 +83,13 @@ namespace CLre sb.AppendFormat("-----------------------------\n"); API.Utility.Logging.Log(sb.ToString()); } + + public override void OnGUI() + { + if (GUI.Button(new Rect(10, 10, 50, 50), "TEST")) + { + + } + } } } diff --git a/CLre/CLre.csproj b/CLre/CLre.csproj index c0c3eee..68c98a3 100644 --- a/CLre/CLre.csproj +++ b/CLre/CLre.csproj @@ -3,7 +3,7 @@ net472 true - 0.0.1 + 0.0.2 NGnius MIT https://git.exmods.org/NGnius/CLre @@ -281,6 +281,9 @@ ..\..\cl\Cardlife_Data\Managed\IllusionPlugin.dll + + + diff --git a/CLre/Fixes/CooldownCrossSlotSync.cs b/CLre/Fixes/CooldownCrossSlotSync.cs new file mode 100644 index 0000000..5e5235f --- /dev/null +++ b/CLre/Fixes/CooldownCrossSlotSync.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Reflection; +using CLre.API.Utility; +using HarmonyLib; +using Svelto.DataStructures; +using Svelto.ECS; + +namespace CLre.Fixes +{ + [Bugfix(name = "CooldownCrossSlotSync", + more = "https://trello.com/c/65FPrTjK/12-no-cooldown-between-inventory-slots", + description = "Apply cooldown to all weapons", + component = BugfixType.HarmonyPatch, id = 4)] + [HarmonyPatch] + class WeaponCooldownEngine_OnQuickAttackStateChange_Patch + { + public static bool Enable = true; + + private static bool isRunningTick = false; + + [HarmonyPostfix] + public static void AfterMethodCall(object __instance, int characterId, bool isPerformingQuickAttack) + { + if (!Enable) return; + // TODO optimise this... a lot + // build functions for retrieving internal Game.Handhelds.CharacterWeaponCooldownEntityView object + IEntitiesDB entitiesDB = (IEntitiesDB) + AccessTools.PropertyGetter(AccessTools.TypeByName("Game.Handhelds.WeaponCooldownEngine"), "entitiesDB") + .Invoke(__instance, new object[0]); + Reflection.ExistsV2 cwcevExists = + Reflection.MethodAsDelegate( + "Svelto.ECS.IEntitiesDB:Exists", + new[] {typeof(int), typeof(int)}, + new[] {AccessTools.TypeByName("Game.Handhelds.CharacterWeaponCooldownEntityView")}, + entitiesDB); + Reflection.QueryEntityViewV2 queryCWCEV = + Reflection.MethodAsDelegate>( + typeof(IEntitiesDB), + "QueryEntityView", + parameters: new[] {typeof(int), typeof(ExclusiveGroup.ExclusiveGroupStruct)}, + generics: new[] {AccessTools.TypeByName("Game.Handhelds.CharacterWeaponCooldownEntityView")}, + entitiesDB); + // retrieve Game.Handhelds.CharacterWeaponCooldownEntityView + if (!cwcevExists(characterId, (int) DEPRECATED_SveltoExtensions.DEPRECATED_GROUP)) + { + //API.Utility.Logging.MetaDebugLog($"No CharacterWeaponCooldownEntityView with id {characterId} found"); + return; + } + + object cwcevOriginal = queryCWCEV(characterId, + (ExclusiveGroup.ExclusiveGroupStruct) DEPRECATED_SveltoExtensions.DEPRECATED_GROUP); + // get id because that's important + int toolId = Traverse.Create(cwcevOriginal).Field("handheldEquipmentComponent") + .Property("equippedHandheldId") + .GetValue>().value; + //API.Utility.Logging.MetaLog("Got CharacterWeaponCooldownEntityView object using nasty code"); + // build functions for retrieving internal Game.Handhelds.WeaponCooldownEntityView object + Reflection.ExistsV2 wcevExists = + Reflection.MethodAsDelegate( + "Svelto.ECS.IEntitiesDB:Exists", + new[] {typeof(int), typeof(int)}, + new[] {AccessTools.TypeByName("Game.Handhelds.WeaponCooldownEntityView")}, + entitiesDB); + Reflection.QueryEntityViewV2 queryWCEV = + Reflection.MethodAsDelegate>( + typeof(IEntitiesDB), + "QueryEntityView", + parameters: new[] {typeof(int), typeof(ExclusiveGroup.ExclusiveGroupStruct)}, + generics: new[] {AccessTools.TypeByName("Game.Handhelds.WeaponCooldownEntityView")}, + entitiesDB); + ExclusiveGroup baseGroup = (ExclusiveGroup) AccessTools + .Field(AccessTools.TypeByName("Game.Handhelds.HandheldGroups"), "BaseGroup").GetValue(null); + // retrieve WeaponCooldownEntityView + if (!wcevExists(toolId, (int) baseGroup)) + { + //API.Utility.Logging.MetaDebugLog($"No WeaponCooldownEntityView with id {toolId} found"); + return; + } + + object wcevOriginal = queryWCEV(toolId, baseGroup); + float cooldownLeft = + Traverse.Create(wcevOriginal).Field("weaponCooldownComponent").Property("cooldownLeft").Value; + bool isInCooldown = + Traverse.Create(wcevOriginal).Field("weaponCooldownComponent").Property("isInCooldown").Value; + //API.Utility.Logging.MetaLog($"Cooling down? {isInCooldown} for {cooldownLeft}s"); + // build functions for querying all Game.Handhelds.WeaponCooldownEntityView objects + Type protoRaw = + typeof(Reflection.QueryEntityViews).GetGenericTypeDefinition() + .MakeGenericType( + AccessTools.TypeByName("Game.Handhelds.WeaponCooldownEntityView")); + Delegate queryAllWCEV = + Reflection.MethodAsDelegateRaw( + typeof(IEntitiesDB), + protoRaw, + "QueryEntityViews", + parameters: new[] {typeof(int)}, + generics: new[] {AccessTools.TypeByName("Game.Handhelds.WeaponCooldownEntityView")}, + entitiesDB); + object collectionStruct = + queryAllWCEV.DynamicInvoke((int) baseGroup); + int count = Traverse.Create(collectionStruct).Field("_count").Value; + PropertyInfo indexer = typeof(Svelto.DataStructures.ReadOnlyCollectionStruct).GetGenericTypeDefinition() + .MakeGenericType(AccessTools.TypeByName("Game.Handhelds.WeaponCooldownEntityView")) + .GetIndexer(); + object[] indexParams = {0}; + for (int index = 0; index < count; index++) + { +#if DEBUG + API.Utility.Logging.MetaLog("Syncing cooldown with another weapon"); +#endif + indexParams[0] = index; + object wcev = indexer.GetValue(collectionStruct, indexParams); + Traverse.Create(wcev) + .Field("weaponCooldownComponent") + .Property("cooldownLeft") + .Value = cooldownLeft; + + Traverse.Create(wcev) + .Field("weaponCooldownComponent") + .Property("isInCooldown").Value = isInCooldown; + } + + if (!isRunningTick) + { + cooldownTickEverything(characterId, entitiesDB, cwcevExists, queryCWCEV, queryWCEV, queryAllWCEV, indexer, baseGroup).Run(); + } + } + + [HarmonyTargetMethod] + public static MethodBase Target() + { + return AccessTools.Method("Game.Handhelds.WeaponCooldownEngine:OnQuickAttackStateChange", + new[] {typeof(int), typeof(bool)}); + } + + private static IEnumerator cooldownTickEverything(int characterId, IEntitiesDB entitiesDb, Reflection.ExistsV2 cwcevExists, Reflection.QueryEntityViewV2 cwcevQuery, Reflection.QueryEntityViewV2 wcevQuery, Delegate wcevQueryAll, PropertyInfo indexer, ExclusiveGroup baseGroup) + { + isRunningTick = true; + while (cwcevExists(characterId, (int) DEPRECATED_SveltoExtensions.DEPRECATED_GROUP)) + { +#if DEBUG + API.Utility.Logging.MetaLog("Doing cooldown tick for all weapons"); +#endif + // get equipped handheld info + object cwcevOriginal = cwcevQuery(characterId, + (ExclusiveGroup.ExclusiveGroupStruct) DEPRECATED_SveltoExtensions.DEPRECATED_GROUP); + int toolId = Traverse.Create(cwcevOriginal).Field("handheldEquipmentComponent") + .Property("equippedHandheldId") + .GetValue>().value; + // get cooldown info for equipped item + object wcevOriginal = wcevQuery(toolId, baseGroup); + object collectionStruct = + wcevQueryAll.DynamicInvoke((int)baseGroup); + int count = Traverse.Create(collectionStruct).Field("_count").Value; + float cooldownLeft = + Traverse.Create(wcevOriginal).Field("weaponCooldownComponent").Property("cooldownLeft").Value; + bool isInCooldown = + Traverse.Create(wcevOriginal).Field("weaponCooldownComponent").Property("isInCooldown").Value; + if (!isInCooldown) break; + object[] indexParams = {0}; + // iterate over other handhelds and sync their cooldowns to the held item + for (int index = 0; index < count; index++) + { + indexParams[0] = index; + object wcev = indexer.GetValue(collectionStruct, indexParams); + Traverse.Create(wcev) + .Field("weaponCooldownComponent") + .Property("cooldownLeft") + .Value = cooldownLeft; + + Traverse.Create(wcev) + .Field("weaponCooldownComponent") + .Property("isInCooldown").Value = isInCooldown; + } + + yield return null; + } + // cleanup + isRunningTick = false; + yield return null; + } + } +} \ No newline at end of file