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);
+ }
+ }
+}