diff --git a/EXILED/Exiled.Events/EventArgs/Scp096/RemovingTargetEventArgs.cs b/EXILED/Exiled.Events/EventArgs/Scp096/RemovingTargetEventArgs.cs new file mode 100644 index 0000000000..1dc1934942 --- /dev/null +++ b/EXILED/Exiled.Events/EventArgs/Scp096/RemovingTargetEventArgs.cs @@ -0,0 +1,59 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.EventArgs.Scp096 +{ + using API.Features; + + using Interfaces; + + using Scp096Role = API.Features.Roles.Scp096Role; + + /// + /// Contains all information after removing a target from SCP-096. + /// + public class RemovingTargetEventArgs : IScp096Event, IDeniableEvent + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public RemovingTargetEventArgs(Player scp096, Player target, bool isAllowed = true) + { + Player = scp096; + Scp096 = scp096.Role.As(); + Target = target; + IsAllowed = isAllowed; + } + + /// + /// Gets the that is controlling SCP-096. + /// + public Player Player { get; } + + /// + public Scp096Role Scp096 { get; } + + /// + /// Gets the being removed as a target. + /// + public Player Target { get; } + + /// + /// Gets or sets a value indicating whether the target is allowed to be removed. + /// + public bool IsAllowed { get; set; } + } +} \ No newline at end of file diff --git a/EXILED/Exiled.Events/Handlers/Scp096.cs b/EXILED/Exiled.Events/Handlers/Scp096.cs index 76a6052fb8..7ddf935fb5 100644 --- a/EXILED/Exiled.Events/Handlers/Scp096.cs +++ b/EXILED/Exiled.Events/Handlers/Scp096.cs @@ -32,6 +32,11 @@ public static class Scp096 /// public static Event AddingTarget { get; set; } = new(); + /// + /// Invoked before removing a target from SCP-096. + /// + public static Event RemovingTarget { get; set; } = new(); + /// /// Invoked before SCP-096 begins prying open a gate. /// @@ -65,6 +70,12 @@ public static class Scp096 /// The instance. public static void OnAddingTarget(AddingTargetEventArgs ev) => AddingTarget.InvokeSafely(ev); + /// + /// Called before removing a target from SCP-096. + /// + /// The instance. + public static void OnRemovingTarget(RemovingTargetEventArgs ev) => RemovingTarget.InvokeSafely(ev); + /// /// Called before SCP-096 begins prying open a gate. /// diff --git a/EXILED/Exiled.Events/Patches/Events/Scp096/RemovingTarget.cs b/EXILED/Exiled.Events/Patches/Events/Scp096/RemovingTarget.cs new file mode 100644 index 0000000000..869988bda6 --- /dev/null +++ b/EXILED/Exiled.Events/Patches/Events/Scp096/RemovingTarget.cs @@ -0,0 +1,149 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) ExMod Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Patches.Events.Scp096 +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Reflection.Emit; + + using API.Features; + using API.Features.Pools; + using Exiled.Events.Attributes; + using Exiled.Events.EventArgs.Scp096; + using HarmonyLib; + using PlayerRoles.PlayableScps.Scp096; + using PlayerRoles.Subroutines; + + using static HarmonyLib.AccessTools; + + /// + /// Patches and . + /// Adds the event. + /// + [EventPatch(typeof(Handlers.Scp096), nameof(Handlers.Scp096.RemovingTarget))] + [HarmonyPatch(typeof(Scp096TargetsTracker))] + internal static class RemovingTarget + { + [HarmonyPatch(nameof(Scp096TargetsTracker.RemoveTarget))] + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Pool.Get(instructions); + + Label retFalseLabel = generator.DefineLabel(); + Label runLabel = generator.DefineLabel(); + + // make game check contains instead of removing then forcibly running our event (so no spam event calls and is still deniable) + newInstructions.Find(instruction => instruction.Calls(Method(typeof(HashSet), nameof(HashSet.Remove)))).operand = Method(typeof(HashSet), nameof(HashSet.Contains)); + + int index = newInstructions.FindIndex(instruction => instruction.opcode == OpCodes.Ret) - 1; + + newInstructions[index].WithLabels(retFalseLabel); + + index -= 1; + + // integrate our condition into first if statement + newInstructions.RemoveAt(index); + + newInstructions.InsertRange( + index, + new CodeInstruction[] + { + // integrate our condition into first if statement + new(OpCodes.Brfalse, retFalseLabel), + + // Player.Get(base.Owner) + new(OpCodes.Ldarg_0), + new(OpCodes.Call, PropertyGetter(typeof(StandardSubroutine), nameof(StandardSubroutine.Owner))), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(ReferenceHub) })), + + // Player.Get(target) + new(OpCodes.Ldarg_1), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(ReferenceHub) })), + + // true + new(OpCodes.Ldc_I4_1), + + // RemovingTargetEventArgs ev = new(scp096, target, isAllowed) + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(RemovingTargetEventArgs))[0]), + new(OpCodes.Dup), + + // Handlers.Scp096.OnRemovingTarget(ev) + new(OpCodes.Call, Method(typeof(Handlers.Scp096), nameof(Handlers.Scp096.OnRemovingTarget))), + + // if (!ev.IsAllowed) + // return; + new(OpCodes.Callvirt, PropertyGetter(typeof(RemovingTargetEventArgs), nameof(RemovingTargetEventArgs.IsAllowed))), + new(OpCodes.Brtrue, runLabel), + }); + + index = newInstructions.FindIndex(instruction => instruction.opcode == OpCodes.Ret) + 1; + + // if allowed, remove target + newInstructions.InsertRange(index, new[] + { + new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]).WithLabels(runLabel), + new(OpCodes.Ldfld, Field(typeof(Scp096TargetsTracker), nameof(Scp096TargetsTracker.Targets))), + new(OpCodes.Ldarg_1), + new(OpCodes.Callvirt, Method(typeof(HashSet), nameof(HashSet.Remove))), + new(OpCodes.Pop), + }); + + for (int z = 0; z < newInstructions.Count; z++) + yield return newInstructions[z]; + + ListPool.Pool.Return(newInstructions); + } + + [HarmonyPatch(nameof(Scp096TargetsTracker.ClearAllTargets))] + [HarmonyTranspiler] + private static IEnumerable Transpiler2(IEnumerable instructions) + { + List newInstructions = ListPool.Pool.Get(instructions); + + object continueLabel = newInstructions.Find(instruction => instruction.opcode == OpCodes.Br_S).operand; + + const int offset = 1; + int index = newInstructions.FindIndex(instruction => instruction.opcode == OpCodes.Stloc_1) + offset; + + newInstructions.InsertRange( + index, + new[] + { + // Player.Get(base.Owner) + new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]), + new(OpCodes.Call, PropertyGetter(typeof(StandardSubroutine), nameof(StandardSubroutine.Owner))), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(ReferenceHub) })), + + // Player.Get(target) + new(OpCodes.Ldloc_1), + new(OpCodes.Call, Method(typeof(Player), nameof(Player.Get), new[] { typeof(ReferenceHub) })), + + // true + new(OpCodes.Ldc_I4_1), + + // RemovingTargetEventArgs ev = new(scp096, target, isAllowed) + new(OpCodes.Newobj, GetDeclaredConstructors(typeof(RemovingTargetEventArgs))[0]), + new(OpCodes.Dup), + + // Handlers.Scp096.OnRemovingTarget(ev) + new(OpCodes.Call, Method(typeof(Handlers.Scp096), nameof(Handlers.Scp096.OnRemovingTarget))), + + // if (!ev.IsAllowed) + // continue; + new(OpCodes.Callvirt, PropertyGetter(typeof(RemovingTargetEventArgs), nameof(RemovingTargetEventArgs.IsAllowed))), + new(OpCodes.Brfalse, continueLabel), + }); + + for (int z = 0; z < newInstructions.Count; z++) + yield return newInstructions[z]; + + ListPool.Pool.Return(newInstructions); + } + } +} \ No newline at end of file