diff --git a/EXILED/Exiled.Events/EventArgs/Scp049/FinishingSenseEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp049/FinishingSenseEventArgs.cs new file mode 100644 index 0000000000..43baf5987b --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Scp049/FinishingSenseEventArgs.cs @@ -0,0 +1,66 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Scp049 +{ + using Exiled.API.Features; + using Exiled.API.Features.Roles; + using Exiled.Events.EventArgs.Interfaces; + + /// + /// Contains all information before SCP-049 finishes his sense ability. + /// + public class FinishingSenseEventArgs : IScp049Event, IDeniableEvent + { + /// + /// Initializes a new instance of the class. + /// + /// The SCP-049 instance triggering the event. + /// + /// + /// The player targeted by SCP-049's Sense ability. + /// + /// + /// The time in seconds before the Sense ability can be used again. + /// + /// + /// Specifies whether the Sense effect is allowed to finish. + /// + /// + public FinishingSenseEventArgs(ReferenceHub scp049, ReferenceHub target, double cooldowntime, bool isAllowed = true) + { + Player = Player.Get(scp049); + Scp049 = Player.Role.As(); + Target = Player.Get(target); + IsAllowed = isAllowed; + CooldownTime = cooldowntime; + } + + /// + public Scp049Role Scp049 { get; } + + /// + /// Gets the player who is controlling SCP-049. + /// + public Player Player { get; } + + /// + /// Gets the player who is SCP-049's active target. Can be null. + /// + public Player Target { get; } + + /// + /// Gets or sets the cooldown duration of the Sense ability. + /// + public double CooldownTime { get; set; } + + /// + /// Gets or sets a value indicating whether the server will finishing or not finishing 049 Sense Ability. + /// + public bool IsAllowed { get; set; } + } +} diff --git a/EXILED/Exiled.Events/Handlers/Scp049.cs b/EXILED/Exiled.Events/Handlers/Scp049.cs index 94bb965746..f4a2010794 100644 --- a/EXILED/Exiled.Events/Handlers/Scp049.cs +++ b/EXILED/Exiled.Events/Handlers/Scp049.cs @@ -32,6 +32,11 @@ public static class Scp049 /// public static Event ActivatingSense { get; set; } = new(); + /// + /// Invoked before SCP-049 finish the good sense of the doctor ability. + /// + public static Event FinishingSense { get; set; } = new(); + /// /// Invoked before SCP-049 uses the call ability. /// @@ -60,6 +65,12 @@ public static class Scp049 /// The instance. public static void OnActivatingSense(ActivatingSenseEventArgs ev) => ActivatingSense.InvokeSafely(ev); + /// + /// Called before SCP-049 finish the good sense of the doctor ability. + /// + /// The instance. + public static void OnFinishingSense(FinishingSenseEventArgs ev) => FinishingSense.InvokeSafely(ev); + /// /// Called before SCP-049 starts the call ability. /// @@ -72,4 +83,4 @@ public static class Scp049 /// The instance. public static void OnAttacking(AttackingEventArgs ev) => Attacking.InvokeSafely(ev); } -} \ No newline at end of file +} diff --git a/EXILED/Exiled.Events/Patches/Events/Scp049/FinishingSense.cs b/EXILED/Exiled.Events/Patches/Events/Scp049/FinishingSense.cs new file mode 100644 index 0000000000..9a63f16e7e --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Events/Scp049/FinishingSense.cs @@ -0,0 +1,258 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Events.Scp049 +{ +#pragma warning disable SA1402 // File may only contain a single type + using System.Collections.Generic; + using System.Reflection.Emit; + + using API.Features.Pools; + using Exiled.Events.Attributes; + using Exiled.Events.EventArgs.Scp049; + using HarmonyLib; + + using PlayerRoles.PlayableScps.Scp049; + using PlayerRoles.Subroutines; + + using static HarmonyLib.AccessTools; + + /// + /// Patches . + /// Adds the event. + /// + [EventPatch(typeof(Handlers.Scp049), nameof(Handlers.Scp049.FinishingSense))] + [HarmonyPatch(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.ServerLoseTarget))] + internal class FinishingSense + { + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + LocalBuilder ev = generator.DeclareLocal(typeof(FinishingSenseEventArgs)); + Label retLabel = generator.DefineLabel(); + + newInstructions.InsertRange(0, new CodeInstruction[] + { + // this.Owner + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Owner))), + + // this.Target + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Target))), + + // Scp049SenseAbility.TargetLostCooldown + new(OpCodes.Ldc_R8, (double)Scp049SenseAbility.TargetLostCooldown), + + // true (IsAllowed) + new(OpCodes.Ldc_I4_1), + + // FinishingSenseEventArgs ev = new FinishingSenseEventArgs(ReferenceHub, ReferenceHub, double, bool) + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(FinishingSenseEventArgs))[0]), + new(OpCodes.Dup), + new(OpCodes.Stloc_S, ev.LocalIndex), + + // Handlers.Scp049.OnFinishingSense(ev); + new(OpCodes.Call, Method(typeof(Handlers.Scp049), nameof(Handlers.Scp049.OnFinishingSense))), + + // if (!ev.IsAllowed) return; + new(OpCodes.Ldloc_S, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.IsAllowed))), + new(OpCodes.Brfalse_S, retLabel), + }); + + // this.Cooldown.Trigger((double)Scp049SenseAbility.TargetLostCooldown) index + int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldc_R8 && (double)i.operand == Scp049SenseAbility.TargetLostCooldown); + + // Replace "this.Cooldown.Trigger((double)Scp049SenseAbility.ReducedCooldown)" with "this.Cooldown.Trigger((double)ev.cooldowntime)" + newInstructions.RemoveAt(index); + newInstructions.InsertRange(index, new CodeInstruction[] + { + new(OpCodes.Ldloc, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.CooldownTime))), + }); + + newInstructions[newInstructions.Count - 1].labels.Add(retLabel); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + } + + /// + /// Patches . + /// Adds the event. + /// + [EventPatch(typeof(Handlers.Scp049), nameof(Handlers.Scp049.FinishingSense))] + [HarmonyPatch(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.ServerProcessKilledPlayer))] + internal class FinishingSense2 + { + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + LocalBuilder ev = generator.DeclareLocal(typeof(FinishingSenseEventArgs)); + + // Continue label for isAllowed check + Label retLabel = generator.DefineLabel(); + + // this.Cooldown.Trigger(Scp049SenseAbility.BaseCooldown) index + int offset = -2; + int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Ldc_R8 && (double)i.operand == Scp049SenseAbility.BaseCooldown) + offset; + + newInstructions.InsertRange(index, new CodeInstruction[] + { + // this.Owner + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Owner))), + + // this.Target + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Target))), + + // double CooldownTime = Scp049SenseAbility.BaseCooldown; + new(OpCodes.Ldc_R8, (double)Scp049SenseAbility.BaseCooldown), + + // true (IsAllowed) + new(OpCodes.Ldc_I4_1), + + // FinishingSenseEventArgs ev = new FinishingSenseEventArgs(ReferenceHub, ReferenceHub, double, bool) + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(FinishingSenseEventArgs))[0]), + new(OpCodes.Dup), + new(OpCodes.Stloc_S, ev.LocalIndex), + + // Handlers.Scp049.OnFinishingSense(ev); + new(OpCodes.Call, Method(typeof(Handlers.Scp049), nameof(Handlers.Scp049.OnFinishingSense))), + + // if (!ev.IsAllowed) return; + new(OpCodes.Ldloc_S, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.IsAllowed))), + new(OpCodes.Brfalse_S, retLabel), + }); + + // this.Cooldown.Trigger(Scp049SenseAbility.BaseCooldown) index + index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldc_R8 && (double)i.operand == Scp049SenseAbility.BaseCooldown); + + newInstructions.RemoveAt(index); + newInstructions.InsertRange(index, new CodeInstruction[] + { + new(OpCodes.Ldloc, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.CooldownTime))), + }); + + newInstructions[newInstructions.Count - 1].labels.Add(retLabel); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + } + + /// + /// Patches . + /// Adds the event. + /// + [EventPatch(typeof(Handlers.Scp049), nameof(Handlers.Scp049.FinishingSense))] + [HarmonyPatch(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.ServerProcessCmd), new[] { typeof(Mirror.NetworkReader) })] + internal class FinishingSense3 + { + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + LocalBuilder ev = generator.DeclareLocal(typeof(FinishingSenseEventArgs)); + LocalBuilder isAbilityActive = generator.DeclareLocal(typeof(bool)); + + Label skipactivatingsense = generator.DefineLabel(); + Label continueLabel = generator.DefineLabel(); + Label allowed = generator.DefineLabel(); + + newInstructions.InsertRange(0, new CodeInstruction[] + { + // isAbilityActive = this.HasTarget; + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.HasTarget))), + new(OpCodes.Stloc, isAbilityActive.LocalIndex), + }); + + // this.Cooldown.Trigger(2.5) index + int offset = -1; + int index = newInstructions.FindIndex(i => + i.opcode == OpCodes.Ldfld && + i.operand == (object)Field(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Cooldown))) + offset; + + newInstructions[index].labels.Add(continueLabel); + + newInstructions.InsertRange(index, new CodeInstruction[] + { + // Skip if the ability is not active and this is an unsuccessful attempt + new(OpCodes.Ldloc, isAbilityActive.LocalIndex), + new(OpCodes.Brfalse_S, continueLabel), + + // this.Owner; + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Owner))), + + // this.Target; + new(OpCodes.Ldarg_0), + new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Target))), + + // Scp049SenseAbility.AttemptFailCooldown; + new(OpCodes.Ldc_R8, (double)Scp049SenseAbility.AttemptFailCooldown), + + // true (IsAllowed) + new(OpCodes.Ldc_I4_1), + + // FinishingSenseEventArgs ev = new FinishingSenseEventArgs(ReferenceHub, ReferenceHub, double, bool) + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(FinishingSenseEventArgs))[0]), + new(OpCodes.Dup), + new(OpCodes.Stloc_S, ev.LocalIndex), + + // Handlers.Scp049.OnFinishingSense(ev); + new(OpCodes.Call, Method(typeof(Handlers.Scp049), nameof(Handlers.Scp049.OnFinishingSense))), + + // if (!ev.IsAllowed) return; + new(OpCodes.Ldloc_S, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.IsAllowed))), + new(OpCodes.Brtrue_S, allowed), + + // If not allowed, set has target to true so as not to break the sense ability + // this.HasTarget = true; + new(OpCodes.Ldarg_0), + new(OpCodes.Ldc_I4_1), + new(OpCodes.Callvirt, PropertySetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.HasTarget))), + + // return; + new(OpCodes.Ret), + + // this.Cooldown.Trigger(ev.cooldown.time) + new CodeInstruction(OpCodes.Ldarg_0).WithLabels(allowed), + new(OpCodes.Ldloc, ev.LocalIndex), + new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.CooldownTime))), + new(OpCodes.Callvirt, Method(typeof(AbilityCooldown), nameof(AbilityCooldown.Trigger), new[] { typeof(double) })), + + new(OpCodes.Br_S, skipactivatingsense), + }); + + offset = -2; + index = newInstructions.FindIndex(i => + i.opcode == OpCodes.Call && + i.operand == (object)Method(typeof(SubroutineBase), nameof(SubroutineBase.ServerSendRpc), new[] { typeof(bool) })) + offset; + + newInstructions[index].labels.Add(skipactivatingsense); + + for (int i = 0; i < newInstructions.Count; i++) + yield return newInstructions[i]; + + ListPool.Pool.Return(newInstructions); + } + } +}