diff --git a/HKMP/Animation/AnimationManager.cs b/HKMP/Animation/AnimationManager.cs index b877f9a..931b3cd 100644 --- a/HKMP/Animation/AnimationManager.cs +++ b/HKMP/Animation/AnimationManager.cs @@ -455,9 +455,6 @@ ServerSettings serverSettings // Register when the player dies to send the animation ModHooks.BeforePlayerDeadHook += OnDeath; - - // Register IL hook for changing the behaviour of tink effects - IL.TinkEffect.OnTriggerEnter2D += TinkEffectOnTriggerEnter2D; // Set the server settings for all animation effects foreach (var effect in AnimationEffects.Values) { @@ -1134,42 +1131,4 @@ public static AnimationClip GetCurrentAnimationClip() { return 0; } - - /// - /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger on remote players. - /// This method will insert IL to check whether the player responsible for the attack is the local player. - /// - private void TinkEffectOnTriggerEnter2D(ILContext il) { - try { - // Create a cursor for this context - var c = new ILCursor(il); - - // Find the first return instruction in the method to branch to later - var retInstr = il.Instrs.First(i => i.MatchRet()); - - // Load the 'collision' argument onto the stack - c.Emit(OpCodes.Ldarg_1); - - // Emit a delegate that pops the TinkEffect from the stack, checks whether the parent - // of the effect is the knight and pushes a bool on the stack based on this - c.EmitDelegate>(collider => { - var parent = collider.transform.parent; - if (parent == null) { - return true; - } - - parent = parent.parent; - if (parent == null) { - return true; - } - - return parent.gameObject.name != "Knight"; - }); - - // Based on the bool we pushed to the stack earlier, we conditionally branch to the return instruction - c.Emit(OpCodes.Brtrue, retInstr); - } catch (Exception e) { - Logger.Error($"Could not change TinkEffect#OnTriggerEnter2D IL:\n{e}"); - } - } } diff --git a/HKMP/Game/Client/ClientManager.cs b/HKMP/Game/Client/ClientManager.cs index b120090..48dbc10 100644 --- a/HKMP/Game/Client/ClientManager.cs +++ b/HKMP/Game/Client/ClientManager.cs @@ -190,6 +190,7 @@ ModSettings modSettings _saveManager.Initialize(); new PauseManager(netClient).RegisterHooks(); + new GamePatcher().RegisterHooks(); new FsmPatcher().RegisterHooks(); _commandManager = new ClientCommandManager(); diff --git a/HKMP/Game/Client/Entity/EntityManager.cs b/HKMP/Game/Client/Entity/EntityManager.cs index f16a5c8..b5ab3b7 100644 --- a/HKMP/Game/Client/Entity/EntityManager.cs +++ b/HKMP/Game/Client/Entity/EntityManager.cs @@ -65,17 +65,6 @@ public EntityManager(NetClient netClient) { UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnSceneChanged; FindGameObject.Find += OnFindGameObject; - - On.BridgeLever.OnTriggerEnter2D += BridgeLeverOnTriggerEnter2D; - - var type = typeof(BridgeLever).GetNestedType("d__13", - BindingFlags.NonPublic | BindingFlags.Instance); - - // TODO: store this hook and unregister if entity system is not used - new ILHook( - type.GetMethod("MoveNext", BindingFlags.NonPublic | BindingFlags.Instance), - BridgeLeverOnOpenBridge - ); } /// @@ -512,123 +501,4 @@ private void OnFindGameObject(FindGameObject.orig_Find orig, HutongGames.PlayMak Logger.Debug(" Name did not match any entity"); } - - /// - /// Whether the local player hit the bridge lever. - /// - private bool _localPlayerBridgeLever; - - /// - /// On Hook that stores a boolean depending on whether the local player hit the bridge lever or not. Used in the - /// IL Hook below. - /// - private void BridgeLeverOnTriggerEnter2D(On.BridgeLever.orig_OnTriggerEnter2D orig, BridgeLever self, Collider2D collision) { - Logger.Debug("BridgeLeverOnTriggerEnter2D"); - - var activated = ReflectionHelper.GetField(self, "activated"); - - Logger.Debug($" activated: {activated}, collision tag: {collision.tag}"); - if (!activated && collision.tag == "Nail Attack") { - _localPlayerBridgeLever = collision.transform.parent?.parent?.tag == "Player"; - Logger.Debug($" Bridge lever hit: bool: {_localPlayerBridgeLever}"); - } - - orig(self, collision); - } - - /// - /// IL Hook to modify the OpenBridge method of BridgeLever to exclude locking players in place that did not hit - /// the lever. - /// - private void BridgeLeverOnOpenBridge(ILContext il) { - Logger.Debug("BridgeLeverOnOpenBridge IL"); - try { - // Create a cursor for this context - var c = new ILCursor(il); - - // Define the collection of instructions that matches the FreezeMoment call - Func[] freezeMomentInstructions = [ - i => i.MatchCall(typeof(global::GameManager), "get_instance"), - i => i.MatchLdcI4(1), - i => i.MatchCallvirt(typeof(global::GameManager), "FreezeMoment") - ]; - - // Goto after the FreezeMoment call - c.GotoNext(MoveType.Before, freezeMomentInstructions); - - // Emit a delegate that puts the boolean on the stack - c.EmitDelegate(() => _localPlayerBridgeLever); - - // Define the label to branch to - var afterFreezeLabel = c.DefineLabel(); - - // Then emit an instruction that branches to after the freeze if the boolean is false - c.Emit(OpCodes.Brfalse, afterFreezeLabel); - - // Goto after the FreezeMoment call - c.GotoNext(MoveType.After, freezeMomentInstructions); - - // Mark the label after the FreezeMoment call so we branch here - c.MarkLabel(afterFreezeLabel); - - // Goto after the rumble call - c.GotoNext( - MoveType.After, - i => i.MatchCall(typeof(GameCameras), "get_instance"), - i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), - i => i.MatchLdstr("RumblingMed"), - i => i.MatchLdcI4(1), - i => i.MatchCall(typeof(FSMUtility), "SetBool") - ); - - // Emit a delegate that puts the boolean on the stack - c.EmitDelegate(() => _localPlayerBridgeLever); - - // Define the label to branch to - var afterRoarEnterLabel = c.DefineLabel(); - - // Emit another instruction that branches over the roar enter FSM calls - c.Emit(OpCodes.Brfalse, afterRoarEnterLabel); - - // Goto after the roar enter call - c.GotoNext( - MoveType.After, - i => i.MatchLdstr("ROAR ENTER"), - i => i.MatchLdcI4(0), - i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") - ); - - // Mark the label after the Roar Enter call so we branch here - c.MarkLabel(afterRoarEnterLabel); - - // Define the collection of instructions that matches the roar exit FSM call - Func[] roarExitInstructions = [ - i => i.MatchCall(typeof(HeroController), "get_instance"), - i => i.MatchCallvirt(typeof(UnityEngine.Component), "get_gameObject"), - i => i.MatchLdstr("ROAR EXIT"), - i => i.MatchLdcI4(0), - i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") - ]; - - // Goto before the roar exit FSM call - c.GotoNext(MoveType.Before, roarExitInstructions); - - // Emit a delegate that puts the boolean on the stack - c.EmitDelegate(() => _localPlayerBridgeLever); - - // Define the label to branch to - var afterRoarExitLabel = c.DefineLabel(); - - // Emit the last instruction to branch over the roar exit call - c.Emit(OpCodes.Brfalse, afterRoarExitLabel); - - // Goto after the roar exit FSM call - c.GotoNext(MoveType.After, roarExitInstructions); - - // Mark the label so we branch here - c.MarkLabel(afterRoarExitLabel); - } catch (Exception e) { - Logger.Error($"Could not change BridgeLever#OnOpenBridge IL: \n{e}"); - } - } } diff --git a/HKMP/Game/Client/GamePatcher.cs b/HKMP/Game/Client/GamePatcher.cs new file mode 100644 index 0000000..d4b4d84 --- /dev/null +++ b/HKMP/Game/Client/GamePatcher.cs @@ -0,0 +1,258 @@ +using System; +using System.Linq; +using System.Reflection; +using Modding; +using Mono.Cecil.Cil; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using UnityEngine; +using Logger = Hkmp.Logging.Logger; + +namespace Hkmp.Game.Client; + +/// +/// Class that manager patches such as IL and On hooks that are standalone patches for the multiplayer to function +/// correctly. +/// +internal class GamePatcher { + /// + /// The binding flags for obtaining certain types for hooking. + /// + private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; + + /// + /// The IL Hook for the bridge lever method. + /// + private ILHook _bridgeLeverIlHook; + + /// + /// Register the hooks. + /// + public void RegisterHooks() { + // Register IL hook for changing the behaviour of tink effects + IL.TinkEffect.OnTriggerEnter2D += TinkEffectOnTriggerEnter2D; + + IL.HealthManager.TakeDamage += HealthManagerOnTakeDamage; + + On.BridgeLever.OnTriggerEnter2D += BridgeLeverOnTriggerEnter2D; + + var type = typeof(BridgeLever).GetNestedType("d__13", BindingFlags); + _bridgeLeverIlHook = new ILHook(type.GetMethod("MoveNext", BindingFlags), BridgeLeverOnOpenBridge); + } + + /// + /// De-register the hooks. + /// + public void DeregisterHooks() { + IL.HealthManager.TakeDamage -= HealthManagerOnTakeDamage; + + On.BridgeLever.OnTriggerEnter2D -= BridgeLeverOnTriggerEnter2D; + + _bridgeLeverIlHook?.Dispose(); + } + + /// + /// IL hook to change the TinkEffect OnTriggerEnter2D to not trigger on remote players. + /// This method will insert IL to check whether the player responsible for the attack is the local player. + /// + private void TinkEffectOnTriggerEnter2D(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Find the first return instruction in the method to branch to later + var retInstr = il.Instrs.First(i => i.MatchRet()); + + // Load the 'collision' argument onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Emit a delegate that pops the TinkEffect from the stack, checks whether the parent + // of the effect is the knight and pushes a bool on the stack based on this + c.EmitDelegate>(collider => { + var parent = collider.transform.parent; + if (parent == null) { + return true; + } + + parent = parent.parent; + if (parent == null) { + return true; + } + + return parent.gameObject.name != "Knight"; + }); + + // Based on the bool we pushed to the stack earlier, we conditionally branch to the return instruction + c.Emit(OpCodes.Brtrue, retInstr); + } catch (Exception e) { + Logger.Error($"Could not change TinkEffect#OnTriggerEnter2D IL:\n{e}"); + } + } + + /// + /// IL Hook to modify the behaviour of the TakeDamage method in HealthManager. This modification adds a + /// conditional branch in case the nail swing from the HitInstance was from a remote player to ensure that + /// soul is not gained for remote hits. + /// + private void HealthManagerOnTakeDamage(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Goto the next virtual call to HeroController.SoulGain() + c.GotoNext(i => i.MatchCallvirt(typeof(HeroController), "SoulGain")); + + // Move the cursor to before the call and call virtual instructions + c.Index -= 1; + + // Emit the instruction to load the first parameter (hitInstance) onto the stack + c.Emit(OpCodes.Ldarg_1); + + // Emit a delegate that takes the hitInstance parameter from the stack and pushes a boolean on the stack + // that indicates whether the hitInstance was from a remote player's nail swing + c.EmitDelegate>(hitInstance => { + if (hitInstance.Source == null || hitInstance.Source.transform == null) { + return false; + } + + // Find the top-level parent of the hit instance + var transform = hitInstance.Source.transform; + while (transform.parent != null) { + transform = transform.parent; + } + + var go = transform.gameObject; + + return go.tag != "Player"; + }); + + // Define a label for the branch instruction + var afterLabel = c.DefineLabel(); + + // Emit the branch (on true) instruction with the label + c.Emit(OpCodes.Brtrue, afterLabel); + + // Move the cursor after the SoulGain method call + c.Index += 2; + + // Mark the label here, so we branch after the SoulGain method call on true + c.MarkLabel(afterLabel); + } catch (Exception e) { + Logger.Error($"Could not change HealthManager#TakeDamage IL:\n{e}"); + } + } + + /// + /// Whether the local player hit the bridge lever. + /// + private bool _localPlayerBridgeLever; + + /// + /// On Hook that stores a boolean depending on whether the local player hit the bridge lever or not. Used in the + /// IL Hook below. + /// + private void BridgeLeverOnTriggerEnter2D(On.BridgeLever.orig_OnTriggerEnter2D orig, BridgeLever self, Collider2D collision) { + var activated = ReflectionHelper.GetField(self, "activated"); + + if (!activated && collision.tag == "Nail Attack") { + _localPlayerBridgeLever = collision.transform.parent?.parent?.tag == "Player"; + } + + orig(self, collision); + } + + /// + /// IL Hook to modify the OpenBridge method of BridgeLever to exclude locking players in place that did not hit + /// the lever. + /// + private void BridgeLeverOnOpenBridge(ILContext il) { + try { + // Create a cursor for this context + var c = new ILCursor(il); + + // Define the collection of instructions that matches the FreezeMoment call + Func[] freezeMomentInstructions = [ + i => i.MatchCall(typeof(global::GameManager), "get_instance"), + i => i.MatchLdcI4(1), + i => i.MatchCallvirt(typeof(global::GameManager), "FreezeMoment") + ]; + + // Goto after the FreezeMoment call + c.GotoNext(MoveType.Before, freezeMomentInstructions); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterFreezeLabel = c.DefineLabel(); + + // Then emit an instruction that branches to after the freeze if the boolean is false + c.Emit(OpCodes.Brfalse, afterFreezeLabel); + + // Goto after the FreezeMoment call + c.GotoNext(MoveType.After, freezeMomentInstructions); + + // Mark the label after the FreezeMoment call so we branch here + c.MarkLabel(afterFreezeLabel); + + // Goto after the rumble call + c.GotoNext( + MoveType.After, + i => i.MatchCall(typeof(GameCameras), "get_instance"), + i => i.MatchLdfld(typeof(GameCameras), "cameraShakeFSM"), + i => i.MatchLdstr("RumblingMed"), + i => i.MatchLdcI4(1), + i => i.MatchCall(typeof(FSMUtility), "SetBool") + ); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterRoarEnterLabel = c.DefineLabel(); + + // Emit another instruction that branches over the roar enter FSM calls + c.Emit(OpCodes.Brfalse, afterRoarEnterLabel); + + // Goto after the roar enter call + c.GotoNext( + MoveType.After, + i => i.MatchLdstr("ROAR ENTER"), + i => i.MatchLdcI4(0), + i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") + ); + + // Mark the label after the Roar Enter call so we branch here + c.MarkLabel(afterRoarEnterLabel); + + // Define the collection of instructions that matches the roar exit FSM call + Func[] roarExitInstructions = [ + i => i.MatchCall(typeof(HeroController), "get_instance"), + i => i.MatchCallvirt(typeof(UnityEngine.Component), "get_gameObject"), + i => i.MatchLdstr("ROAR EXIT"), + i => i.MatchLdcI4(0), + i => i.MatchCall(typeof(FSMUtility), "SendEventToGameObject") + ]; + + // Goto before the roar exit FSM call + c.GotoNext(MoveType.Before, roarExitInstructions); + + // Emit a delegate that puts the boolean on the stack + c.EmitDelegate(() => _localPlayerBridgeLever); + + // Define the label to branch to + var afterRoarExitLabel = c.DefineLabel(); + + // Emit the last instruction to branch over the roar exit call + c.Emit(OpCodes.Brfalse, afterRoarExitLabel); + + // Goto after the roar exit FSM call + c.GotoNext(MoveType.After, roarExitInstructions); + + // Mark the label so we branch here + c.MarkLabel(afterRoarExitLabel); + } catch (Exception e) { + Logger.Error($"Could not change BridgeLever#OnOpenBridge IL: \n{e}"); + } + } +} diff --git a/HKMP/Game/Client/PlayerManager.cs b/HKMP/Game/Client/PlayerManager.cs index 165d334..ff2ec59 100644 --- a/HKMP/Game/Client/PlayerManager.cs +++ b/HKMP/Game/Client/PlayerManager.cs @@ -109,8 +109,6 @@ Dictionary playerData OnPlayerTeamUpdate); packetManager.RegisterClientPacketHandler(ClientPacketId.PlayerSkinUpdate, OnPlayerSkinUpdate); - - IL.HealthManager.TakeDamage += HealthManagerOnTakeDamage; } /// @@ -784,57 +782,4 @@ private void ToggleBodyDamage(ClientPlayerData playerData, bool enabled) { playerObject.GetComponent().enabled = false; } } - - /// - /// IL Hook to modify the behaviour of the TakeDamage method in HealthManager. This modification adds a - /// conditional branch in case the nail swing from the HitInstance was from a remote player to ensure that - /// soul is not gained for remote hits. - /// - private void HealthManagerOnTakeDamage(ILContext il) { - try { - // Create a cursor for this context - var c = new ILCursor(il); - - // Goto the next virtual call to HeroController.SoulGain() - c.GotoNext(i => i.MatchCallvirt(typeof(HeroController), "SoulGain")); - - // Move the cursor to before the call and call virtual instructions - c.Index -= 1; - - // Emit the instruction to load the first parameter (hitInstance) onto the stack - c.Emit(OpCodes.Ldarg_1); - - // Emit a delegate that takes the hitInstance parameter from the stack and pushes a boolean on the stack - // that indicates whether the hitInstance was from a remote player's nail swing - c.EmitDelegate>(hitInstance => { - if (hitInstance.Source == null || hitInstance.Source.transform == null) { - return false; - } - - // Find the top-level parent of the hit instance - var transform = hitInstance.Source.transform; - while (transform.parent != null) { - transform = transform.parent; - } - - var go = transform.gameObject; - - return go.tag != "Player"; - }); - - // Define a label for the branch instruction - var afterLabel = c.DefineLabel(); - - // Emit the branch (on true) instruction with the label - c.Emit(OpCodes.Brtrue, afterLabel); - - // Move the cursor after the SoulGain method call - c.Index += 2; - - // Mark the label here, so we branch after the SoulGain method call on true - c.MarkLabel(afterLabel); - } catch (Exception e) { - Logger.Error($"Could not change HealthManager#TakeDamage IL:\n{e}"); - } - } }